@rogieking/figui3 3.2.0 → 3.4.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/components.css +280 -35
- package/dist/fig.js +38 -38
- package/fig.js +628 -65
- package/package.json +1 -1
package/fig.js
CHANGED
|
@@ -2253,15 +2253,15 @@ class FigTabs extends HTMLElement {
|
|
|
2253
2253
|
this.setAttribute("role", "tablist");
|
|
2254
2254
|
this.addEventListener("click", this.handleClick.bind(this));
|
|
2255
2255
|
this.addEventListener("keydown", this.#handleKeyDown.bind(this));
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
}
|
|
2256
|
+
requestAnimationFrame(() => {
|
|
2257
|
+
const value = this.getAttribute("value");
|
|
2258
|
+
if (value) {
|
|
2259
|
+
this.#selectByValue(value);
|
|
2260
|
+
}
|
|
2261
|
+
if (this.hasAttribute("disabled")) {
|
|
2262
|
+
this.#applyDisabled(true);
|
|
2263
|
+
}
|
|
2264
|
+
});
|
|
2265
2265
|
}
|
|
2266
2266
|
|
|
2267
2267
|
#applyDisabled(disabled) {
|
|
@@ -2333,6 +2333,7 @@ class FigTabs extends HTMLElement {
|
|
|
2333
2333
|
for (const tab of tabs) {
|
|
2334
2334
|
if (tab.getAttribute("value") === value) {
|
|
2335
2335
|
this.selectedTab = tab;
|
|
2336
|
+
tab.setAttribute("selected", "true");
|
|
2336
2337
|
} else {
|
|
2337
2338
|
tab.removeAttribute("selected");
|
|
2338
2339
|
}
|
|
@@ -2353,7 +2354,6 @@ class FigTabs extends HTMLElement {
|
|
|
2353
2354
|
}
|
|
2354
2355
|
|
|
2355
2356
|
handleClick(event) {
|
|
2356
|
-
// Ignore clicks when disabled
|
|
2357
2357
|
if (this.hasAttribute("disabled")) return;
|
|
2358
2358
|
const target = event.target;
|
|
2359
2359
|
if (target.nodeName.toLowerCase() === "fig-tab") {
|
|
@@ -2361,6 +2361,7 @@ class FigTabs extends HTMLElement {
|
|
|
2361
2361
|
for (const tab of tabs) {
|
|
2362
2362
|
if (tab === target) {
|
|
2363
2363
|
this.selectedTab = tab;
|
|
2364
|
+
tab.setAttribute("selected", "true");
|
|
2364
2365
|
this.setAttribute("value", tab.getAttribute("value") || "");
|
|
2365
2366
|
} else {
|
|
2366
2367
|
tab.removeAttribute("selected");
|
|
@@ -3459,20 +3460,46 @@ class FigInputNumber extends HTMLElement {
|
|
|
3459
3460
|
#unitPosition;
|
|
3460
3461
|
#precision;
|
|
3461
3462
|
#isInteracting = false;
|
|
3463
|
+
#stepperEl = null;
|
|
3464
|
+
|
|
3465
|
+
#syncSteppers(hasSteppers) {
|
|
3466
|
+
if (hasSteppers && !this.#stepperEl) {
|
|
3467
|
+
this.#stepperEl = document.createElement("span");
|
|
3468
|
+
this.#stepperEl.className = "fig-steppers";
|
|
3469
|
+
this.#stepperEl.innerHTML =
|
|
3470
|
+
`<button class="fig-stepper-up" tabindex="-1" aria-label="Increase"></button>` +
|
|
3471
|
+
`<button class="fig-stepper-down" tabindex="-1" aria-label="Decrease"></button>`;
|
|
3472
|
+
this.#stepperEl.addEventListener("pointerdown", (e) => {
|
|
3473
|
+
e.preventDefault();
|
|
3474
|
+
e.stopPropagation();
|
|
3475
|
+
const btn = e.target.closest("button");
|
|
3476
|
+
if (!btn || this.disabled) return;
|
|
3477
|
+
const dir = btn.classList.contains("fig-stepper-up") ? 1 : -1;
|
|
3478
|
+
this.#stepValue(dir);
|
|
3479
|
+
this.input.focus();
|
|
3480
|
+
});
|
|
3481
|
+
this.append(this.#stepperEl);
|
|
3482
|
+
} else if (!hasSteppers && this.#stepperEl) {
|
|
3483
|
+
this.#stepperEl.remove();
|
|
3484
|
+
this.#stepperEl = null;
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3462
3487
|
|
|
3463
|
-
#
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
this.
|
|
3474
|
-
|
|
3475
|
-
|
|
3488
|
+
#stepValue(direction) {
|
|
3489
|
+
const step = this.step || 1;
|
|
3490
|
+
let numericValue = this.#getNumericValue(this.input.value);
|
|
3491
|
+
let value =
|
|
3492
|
+
(numericValue !== "" ? Number(numericValue) / (this.transform || 1) : 0) +
|
|
3493
|
+
step * direction;
|
|
3494
|
+
value = this.#sanitizeInput(value, false);
|
|
3495
|
+
this.value = value;
|
|
3496
|
+
this.input.value = this.#formatWithUnit(this.value);
|
|
3497
|
+
this.dispatchEvent(
|
|
3498
|
+
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
3499
|
+
);
|
|
3500
|
+
this.dispatchEvent(
|
|
3501
|
+
new CustomEvent("change", { detail: this.value, bubbles: true }),
|
|
3502
|
+
);
|
|
3476
3503
|
}
|
|
3477
3504
|
|
|
3478
3505
|
constructor() {
|
|
@@ -3528,19 +3555,12 @@ class FigInputNumber extends HTMLElement {
|
|
|
3528
3555
|
this.hasAttribute("steppers") &&
|
|
3529
3556
|
this.getAttribute("steppers") !== "false";
|
|
3530
3557
|
|
|
3531
|
-
// Use type="number" when steppers are enabled (for native spin buttons)
|
|
3532
|
-
const inputType = hasSteppers ? "number" : "text";
|
|
3533
|
-
const inputMode = hasSteppers ? "" : 'inputmode="decimal"';
|
|
3534
|
-
const inputValue = hasSteppers
|
|
3535
|
-
? this.#transformNumber(this.value)
|
|
3536
|
-
: this.#formatWithUnit(this.value);
|
|
3537
|
-
|
|
3538
3558
|
let html = `<input
|
|
3539
|
-
type="
|
|
3540
|
-
|
|
3559
|
+
type="text"
|
|
3560
|
+
inputmode="decimal"
|
|
3541
3561
|
${this.name ? `name="${this.name}"` : ""}
|
|
3542
3562
|
placeholder="${this.placeholder}"
|
|
3543
|
-
value="${
|
|
3563
|
+
value="${this.#formatWithUnit(this.value)}" />`;
|
|
3544
3564
|
|
|
3545
3565
|
//child nodes hack
|
|
3546
3566
|
requestAnimationFrame(() => {
|
|
@@ -3569,7 +3589,8 @@ class FigInputNumber extends HTMLElement {
|
|
|
3569
3589
|
if (this.getAttribute("step")) {
|
|
3570
3590
|
this.step = Number(this.getAttribute("step"));
|
|
3571
3591
|
}
|
|
3572
|
-
|
|
3592
|
+
|
|
3593
|
+
this.#syncSteppers(hasSteppers);
|
|
3573
3594
|
|
|
3574
3595
|
// Set disabled state if present
|
|
3575
3596
|
if (this.hasAttribute("disabled")) {
|
|
@@ -3845,24 +3866,15 @@ class FigInputNumber extends HTMLElement {
|
|
|
3845
3866
|
break;
|
|
3846
3867
|
case "units":
|
|
3847
3868
|
this.#units = newValue || "";
|
|
3848
|
-
this.input.value =
|
|
3849
|
-
this.input.type === "number"
|
|
3850
|
-
? this.#transformNumber(this.value)
|
|
3851
|
-
: this.#formatWithUnit(this.value);
|
|
3869
|
+
this.input.value = this.#formatWithUnit(this.value);
|
|
3852
3870
|
break;
|
|
3853
3871
|
case "unit-position":
|
|
3854
3872
|
this.#unitPosition = newValue || "suffix";
|
|
3855
|
-
this.input.value =
|
|
3856
|
-
this.input.type === "number"
|
|
3857
|
-
? this.#transformNumber(this.value)
|
|
3858
|
-
: this.#formatWithUnit(this.value);
|
|
3873
|
+
this.input.value = this.#formatWithUnit(this.value);
|
|
3859
3874
|
break;
|
|
3860
3875
|
case "transform":
|
|
3861
3876
|
this.transform = Number(newValue) || 1;
|
|
3862
|
-
this.input.value =
|
|
3863
|
-
this.input.type === "number"
|
|
3864
|
-
? this.#transformNumber(this.value)
|
|
3865
|
-
: this.#formatWithUnit(this.value);
|
|
3877
|
+
this.input.value = this.#formatWithUnit(this.value);
|
|
3866
3878
|
break;
|
|
3867
3879
|
case "value":
|
|
3868
3880
|
if (this.#isInteracting) break;
|
|
@@ -3872,41 +3884,25 @@ class FigInputNumber extends HTMLElement {
|
|
|
3872
3884
|
value = this.#sanitizeInput(value, false);
|
|
3873
3885
|
}
|
|
3874
3886
|
this.value = value;
|
|
3875
|
-
this.input.value =
|
|
3876
|
-
this.input.type === "number"
|
|
3877
|
-
? this.#transformNumber(this.value)
|
|
3878
|
-
: this.#formatWithUnit(this.value);
|
|
3887
|
+
this.input.value = this.#formatWithUnit(this.value);
|
|
3879
3888
|
break;
|
|
3880
3889
|
case "min":
|
|
3881
3890
|
case "max":
|
|
3882
3891
|
case "step":
|
|
3883
3892
|
if (newValue === null || newValue === "") {
|
|
3884
3893
|
this[name] = undefined;
|
|
3885
|
-
this.#syncNativeNumberAttributes();
|
|
3886
3894
|
break;
|
|
3887
3895
|
}
|
|
3888
3896
|
this[name] = Number(newValue);
|
|
3889
|
-
this.#syncNativeNumberAttributes();
|
|
3890
3897
|
break;
|
|
3891
3898
|
case "steppers": {
|
|
3892
3899
|
const hasSteppers = newValue !== null && newValue !== "false";
|
|
3893
|
-
this
|
|
3894
|
-
if (hasSteppers) {
|
|
3895
|
-
this.input.removeAttribute("inputmode");
|
|
3896
|
-
this.#syncNativeNumberAttributes();
|
|
3897
|
-
this.input.value = this.#transformNumber(this.value);
|
|
3898
|
-
} else {
|
|
3899
|
-
this.input.setAttribute("inputmode", "decimal");
|
|
3900
|
-
this.input.value = this.#formatWithUnit(this.value);
|
|
3901
|
-
}
|
|
3900
|
+
this.#syncSteppers(hasSteppers);
|
|
3902
3901
|
break;
|
|
3903
3902
|
}
|
|
3904
3903
|
case "precision":
|
|
3905
3904
|
this.#precision = newValue !== null ? Number(newValue) : 2;
|
|
3906
|
-
this.input.value =
|
|
3907
|
-
this.input.type === "number"
|
|
3908
|
-
? this.#transformNumber(this.value)
|
|
3909
|
-
: this.#formatWithUnit(this.value);
|
|
3905
|
+
this.input.value = this.#formatWithUnit(this.value);
|
|
3910
3906
|
break;
|
|
3911
3907
|
case "name":
|
|
3912
3908
|
this[name] = this.input[name] = newValue;
|
|
@@ -5425,10 +5421,18 @@ class FigToast extends HTMLDialogElement {
|
|
|
5425
5421
|
}
|
|
5426
5422
|
}
|
|
5427
5423
|
|
|
5424
|
+
#resolveAutoTheme() {
|
|
5425
|
+
if (this.getAttribute("theme") !== "auto") return;
|
|
5426
|
+
const cs = getComputedStyle(document.documentElement).colorScheme || "";
|
|
5427
|
+
const isDark = cs.includes("dark");
|
|
5428
|
+
this.style.colorScheme = isDark ? "light" : "dark";
|
|
5429
|
+
}
|
|
5430
|
+
|
|
5428
5431
|
/**
|
|
5429
5432
|
* Show the toast notification (non-modal)
|
|
5430
5433
|
*/
|
|
5431
5434
|
showToast() {
|
|
5435
|
+
this.#resolveAutoTheme();
|
|
5432
5436
|
this.show(); // Non-modal show
|
|
5433
5437
|
this.applyPosition();
|
|
5434
5438
|
this.startAutoClose();
|
|
@@ -5460,6 +5464,14 @@ class FigToast extends HTMLDialogElement {
|
|
|
5460
5464
|
this.hideToast();
|
|
5461
5465
|
}
|
|
5462
5466
|
}
|
|
5467
|
+
|
|
5468
|
+
if (name === "theme") {
|
|
5469
|
+
if (newValue === "auto") {
|
|
5470
|
+
this.#resolveAutoTheme();
|
|
5471
|
+
} else {
|
|
5472
|
+
this.style.removeProperty("color-scheme");
|
|
5473
|
+
}
|
|
5474
|
+
}
|
|
5463
5475
|
}
|
|
5464
5476
|
}
|
|
5465
5477
|
figDefineCustomizedBuiltIn("fig-toast", FigToast, { extends: "dialog" });
|
|
@@ -10390,3 +10402,554 @@ class FigFillPicker extends HTMLElement {
|
|
|
10390
10402
|
}
|
|
10391
10403
|
}
|
|
10392
10404
|
customElements.define("fig-fill-picker", FigFillPicker);
|
|
10405
|
+
|
|
10406
|
+
/* Choice */
|
|
10407
|
+
/**
|
|
10408
|
+
* A generic choice container for use within FigChooser.
|
|
10409
|
+
* @attr {string} value - Identifier for this choice
|
|
10410
|
+
* @attr {boolean} selected - Whether this choice is currently selected
|
|
10411
|
+
* @attr {boolean} disabled - Whether this choice is disabled
|
|
10412
|
+
*/
|
|
10413
|
+
class FigChoice extends HTMLElement {
|
|
10414
|
+
static get observedAttributes() {
|
|
10415
|
+
return ["selected", "disabled"];
|
|
10416
|
+
}
|
|
10417
|
+
|
|
10418
|
+
connectedCallback() {
|
|
10419
|
+
this.setAttribute("role", "option");
|
|
10420
|
+
if (!this.hasAttribute("tabindex")) {
|
|
10421
|
+
this.setAttribute("tabindex", "0");
|
|
10422
|
+
}
|
|
10423
|
+
this.setAttribute(
|
|
10424
|
+
"aria-selected",
|
|
10425
|
+
this.hasAttribute("selected") ? "true" : "false",
|
|
10426
|
+
);
|
|
10427
|
+
if (this.hasAttribute("disabled")) {
|
|
10428
|
+
this.setAttribute("aria-disabled", "true");
|
|
10429
|
+
}
|
|
10430
|
+
}
|
|
10431
|
+
|
|
10432
|
+
attributeChangedCallback(name) {
|
|
10433
|
+
if (name === "selected") {
|
|
10434
|
+
this.setAttribute(
|
|
10435
|
+
"aria-selected",
|
|
10436
|
+
this.hasAttribute("selected") ? "true" : "false",
|
|
10437
|
+
);
|
|
10438
|
+
}
|
|
10439
|
+
if (name === "disabled") {
|
|
10440
|
+
const isDisabled =
|
|
10441
|
+
this.hasAttribute("disabled") &&
|
|
10442
|
+
this.getAttribute("disabled") !== "false";
|
|
10443
|
+
if (isDisabled) {
|
|
10444
|
+
this.setAttribute("aria-disabled", "true");
|
|
10445
|
+
this.setAttribute("tabindex", "-1");
|
|
10446
|
+
} else {
|
|
10447
|
+
this.removeAttribute("aria-disabled");
|
|
10448
|
+
this.setAttribute("tabindex", "0");
|
|
10449
|
+
}
|
|
10450
|
+
}
|
|
10451
|
+
}
|
|
10452
|
+
}
|
|
10453
|
+
customElements.define("fig-choice", FigChoice);
|
|
10454
|
+
|
|
10455
|
+
/* Chooser */
|
|
10456
|
+
/**
|
|
10457
|
+
* A selection controller for a list of choice elements.
|
|
10458
|
+
* @attr {string} choice-element - CSS selector for child choices (default: "fig-choice")
|
|
10459
|
+
* @attr {string} layout - Layout mode: "vertical" (default), "horizontal", "grid"
|
|
10460
|
+
* @attr {string} value - Value of the currently selected choice
|
|
10461
|
+
* @attr {boolean} disabled - Whether the chooser is disabled
|
|
10462
|
+
*/
|
|
10463
|
+
class FigChooser extends HTMLElement {
|
|
10464
|
+
#selectedChoice = null;
|
|
10465
|
+
#boundHandleClick = this.#handleClick.bind(this);
|
|
10466
|
+
#boundHandleKeyDown = this.#handleKeyDown.bind(this);
|
|
10467
|
+
#boundSyncOverflow = this.#syncOverflow.bind(this);
|
|
10468
|
+
#mutationObserver = null;
|
|
10469
|
+
#resizeObserver = null;
|
|
10470
|
+
#navStart = null;
|
|
10471
|
+
#navEnd = null;
|
|
10472
|
+
#dragState = null;
|
|
10473
|
+
|
|
10474
|
+
constructor() {
|
|
10475
|
+
super();
|
|
10476
|
+
}
|
|
10477
|
+
|
|
10478
|
+
static get observedAttributes() {
|
|
10479
|
+
return ["value", "disabled", "choice-element", "drag", "overflow", "loop"];
|
|
10480
|
+
}
|
|
10481
|
+
|
|
10482
|
+
get #overflowMode() {
|
|
10483
|
+
return this.getAttribute("overflow") === "scrollbar" ? "scrollbar" : "buttons";
|
|
10484
|
+
}
|
|
10485
|
+
|
|
10486
|
+
get #dragEnabled() {
|
|
10487
|
+
const attr = this.getAttribute("drag");
|
|
10488
|
+
return attr === null || attr !== "false";
|
|
10489
|
+
}
|
|
10490
|
+
|
|
10491
|
+
get #choiceSelector() {
|
|
10492
|
+
return this.getAttribute("choice-element") || "fig-choice";
|
|
10493
|
+
}
|
|
10494
|
+
|
|
10495
|
+
get choices() {
|
|
10496
|
+
return Array.from(this.querySelectorAll(this.#choiceSelector));
|
|
10497
|
+
}
|
|
10498
|
+
|
|
10499
|
+
get selectedChoice() {
|
|
10500
|
+
return this.#selectedChoice;
|
|
10501
|
+
}
|
|
10502
|
+
|
|
10503
|
+
set selectedChoice(element) {
|
|
10504
|
+
if (element && !this.contains(element)) return;
|
|
10505
|
+
const choices = this.choices;
|
|
10506
|
+
for (const choice of choices) {
|
|
10507
|
+
const shouldSelect = choice === element;
|
|
10508
|
+
const isSelected = choice.hasAttribute("selected");
|
|
10509
|
+
if (shouldSelect && !isSelected) {
|
|
10510
|
+
choice.setAttribute("selected", "");
|
|
10511
|
+
} else if (!shouldSelect && isSelected) {
|
|
10512
|
+
choice.removeAttribute("selected");
|
|
10513
|
+
}
|
|
10514
|
+
}
|
|
10515
|
+
this.#selectedChoice = element;
|
|
10516
|
+
const val = element?.getAttribute("value") ?? "";
|
|
10517
|
+
if (this.getAttribute("value") !== val) {
|
|
10518
|
+
if (val) {
|
|
10519
|
+
this.setAttribute("value", val);
|
|
10520
|
+
}
|
|
10521
|
+
}
|
|
10522
|
+
this.#scrollToChoice(element);
|
|
10523
|
+
}
|
|
10524
|
+
|
|
10525
|
+
get value() {
|
|
10526
|
+
return this.#selectedChoice?.getAttribute("value") ?? "";
|
|
10527
|
+
}
|
|
10528
|
+
|
|
10529
|
+
set value(val) {
|
|
10530
|
+
if (val === null || val === undefined || val === "") return;
|
|
10531
|
+
this.setAttribute("value", String(val));
|
|
10532
|
+
}
|
|
10533
|
+
|
|
10534
|
+
connectedCallback() {
|
|
10535
|
+
this.setAttribute("role", "listbox");
|
|
10536
|
+
this.addEventListener("click", this.#boundHandleClick);
|
|
10537
|
+
this.addEventListener("keydown", this.#boundHandleKeyDown);
|
|
10538
|
+
this.addEventListener("scroll", this.#boundSyncOverflow);
|
|
10539
|
+
this.#applyOverflowMode();
|
|
10540
|
+
this.#setupDrag();
|
|
10541
|
+
this.#startObserver();
|
|
10542
|
+
this.#startResizeObserver();
|
|
10543
|
+
|
|
10544
|
+
requestAnimationFrame(() => {
|
|
10545
|
+
this.#syncSelection();
|
|
10546
|
+
this.#syncOverflow();
|
|
10547
|
+
});
|
|
10548
|
+
}
|
|
10549
|
+
|
|
10550
|
+
disconnectedCallback() {
|
|
10551
|
+
this.removeEventListener("click", this.#boundHandleClick);
|
|
10552
|
+
this.removeEventListener("keydown", this.#boundHandleKeyDown);
|
|
10553
|
+
this.removeEventListener("scroll", this.#boundSyncOverflow);
|
|
10554
|
+
this.#teardownDrag();
|
|
10555
|
+
this.#mutationObserver?.disconnect();
|
|
10556
|
+
this.#mutationObserver = null;
|
|
10557
|
+
this.#resizeObserver?.disconnect();
|
|
10558
|
+
this.#resizeObserver = null;
|
|
10559
|
+
this.#removeNavButtons();
|
|
10560
|
+
}
|
|
10561
|
+
|
|
10562
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
10563
|
+
if (name === "value" && newValue !== oldValue && newValue) {
|
|
10564
|
+
this.#selectByValue(newValue);
|
|
10565
|
+
}
|
|
10566
|
+
if (name === "disabled") {
|
|
10567
|
+
const isDisabled = newValue !== null && newValue !== "false";
|
|
10568
|
+
const choices = this.choices;
|
|
10569
|
+
for (const choice of choices) {
|
|
10570
|
+
if (isDisabled) {
|
|
10571
|
+
choice.setAttribute("aria-disabled", "true");
|
|
10572
|
+
choice.setAttribute("tabindex", "-1");
|
|
10573
|
+
} else {
|
|
10574
|
+
choice.removeAttribute("aria-disabled");
|
|
10575
|
+
choice.setAttribute("tabindex", "0");
|
|
10576
|
+
}
|
|
10577
|
+
}
|
|
10578
|
+
}
|
|
10579
|
+
if (name === "choice-element") {
|
|
10580
|
+
requestAnimationFrame(() => this.#syncSelection());
|
|
10581
|
+
}
|
|
10582
|
+
if (name === "drag") {
|
|
10583
|
+
if (this.#dragEnabled) {
|
|
10584
|
+
this.#setupDrag();
|
|
10585
|
+
} else {
|
|
10586
|
+
this.#teardownDrag();
|
|
10587
|
+
}
|
|
10588
|
+
}
|
|
10589
|
+
if (name === "overflow") {
|
|
10590
|
+
this.#applyOverflowMode();
|
|
10591
|
+
}
|
|
10592
|
+
}
|
|
10593
|
+
|
|
10594
|
+
#syncSelection() {
|
|
10595
|
+
const choices = this.choices;
|
|
10596
|
+
if (!choices.length) {
|
|
10597
|
+
this.#selectedChoice = null;
|
|
10598
|
+
return;
|
|
10599
|
+
}
|
|
10600
|
+
|
|
10601
|
+
const valueAttr = this.getAttribute("value");
|
|
10602
|
+
if (valueAttr && this.#selectByValue(valueAttr)) return;
|
|
10603
|
+
|
|
10604
|
+
const alreadySelected = choices.find((c) => c.hasAttribute("selected"));
|
|
10605
|
+
if (alreadySelected) {
|
|
10606
|
+
this.selectedChoice = alreadySelected;
|
|
10607
|
+
return;
|
|
10608
|
+
}
|
|
10609
|
+
|
|
10610
|
+
this.selectedChoice = choices[0];
|
|
10611
|
+
}
|
|
10612
|
+
|
|
10613
|
+
#selectByValue(value) {
|
|
10614
|
+
const choices = this.choices;
|
|
10615
|
+
for (const choice of choices) {
|
|
10616
|
+
if (choice.getAttribute("value") === value) {
|
|
10617
|
+
this.selectedChoice = choice;
|
|
10618
|
+
return true;
|
|
10619
|
+
}
|
|
10620
|
+
}
|
|
10621
|
+
return false;
|
|
10622
|
+
}
|
|
10623
|
+
|
|
10624
|
+
#findChoiceFromTarget(target) {
|
|
10625
|
+
const selector = this.#choiceSelector;
|
|
10626
|
+
let el = target;
|
|
10627
|
+
while (el && el !== this) {
|
|
10628
|
+
if (el.matches(selector)) return el;
|
|
10629
|
+
el = el.parentElement;
|
|
10630
|
+
}
|
|
10631
|
+
return null;
|
|
10632
|
+
}
|
|
10633
|
+
|
|
10634
|
+
#handleClick(event) {
|
|
10635
|
+
if (
|
|
10636
|
+
this.hasAttribute("disabled") &&
|
|
10637
|
+
this.getAttribute("disabled") !== "false"
|
|
10638
|
+
)
|
|
10639
|
+
return;
|
|
10640
|
+
const choice = this.#findChoiceFromTarget(event.target);
|
|
10641
|
+
if (!choice) return;
|
|
10642
|
+
if (
|
|
10643
|
+
choice.hasAttribute("disabled") &&
|
|
10644
|
+
choice.getAttribute("disabled") !== "false"
|
|
10645
|
+
)
|
|
10646
|
+
return;
|
|
10647
|
+
this.selectedChoice = choice;
|
|
10648
|
+
this.#emitEvents();
|
|
10649
|
+
}
|
|
10650
|
+
|
|
10651
|
+
#handleKeyDown(event) {
|
|
10652
|
+
if (
|
|
10653
|
+
this.hasAttribute("disabled") &&
|
|
10654
|
+
this.getAttribute("disabled") !== "false"
|
|
10655
|
+
)
|
|
10656
|
+
return;
|
|
10657
|
+
const choices = this.choices.filter(
|
|
10658
|
+
(c) =>
|
|
10659
|
+
!c.hasAttribute("disabled") || c.getAttribute("disabled") === "false",
|
|
10660
|
+
);
|
|
10661
|
+
if (!choices.length) return;
|
|
10662
|
+
const currentIndex = choices.indexOf(this.#selectedChoice);
|
|
10663
|
+
let nextIndex = currentIndex;
|
|
10664
|
+
|
|
10665
|
+
const loop = this.hasAttribute("loop");
|
|
10666
|
+
|
|
10667
|
+
switch (event.key) {
|
|
10668
|
+
case "ArrowDown":
|
|
10669
|
+
case "ArrowRight":
|
|
10670
|
+
event.preventDefault();
|
|
10671
|
+
if (currentIndex < choices.length - 1) {
|
|
10672
|
+
nextIndex = currentIndex + 1;
|
|
10673
|
+
} else if (loop) {
|
|
10674
|
+
nextIndex = 0;
|
|
10675
|
+
}
|
|
10676
|
+
break;
|
|
10677
|
+
case "ArrowUp":
|
|
10678
|
+
case "ArrowLeft":
|
|
10679
|
+
event.preventDefault();
|
|
10680
|
+
if (currentIndex > 0) {
|
|
10681
|
+
nextIndex = currentIndex - 1;
|
|
10682
|
+
} else if (loop) {
|
|
10683
|
+
nextIndex = choices.length - 1;
|
|
10684
|
+
}
|
|
10685
|
+
break;
|
|
10686
|
+
case "Home":
|
|
10687
|
+
event.preventDefault();
|
|
10688
|
+
nextIndex = 0;
|
|
10689
|
+
break;
|
|
10690
|
+
case "End":
|
|
10691
|
+
event.preventDefault();
|
|
10692
|
+
nextIndex = choices.length - 1;
|
|
10693
|
+
break;
|
|
10694
|
+
case "Enter":
|
|
10695
|
+
case " ":
|
|
10696
|
+
event.preventDefault();
|
|
10697
|
+
if (document.activeElement?.matches(this.#choiceSelector)) {
|
|
10698
|
+
const focused = this.#findChoiceFromTarget(document.activeElement);
|
|
10699
|
+
if (focused && focused !== this.#selectedChoice) {
|
|
10700
|
+
this.selectedChoice = focused;
|
|
10701
|
+
this.#emitEvents();
|
|
10702
|
+
}
|
|
10703
|
+
}
|
|
10704
|
+
return;
|
|
10705
|
+
default:
|
|
10706
|
+
return;
|
|
10707
|
+
}
|
|
10708
|
+
|
|
10709
|
+
if (nextIndex !== currentIndex && choices[nextIndex]) {
|
|
10710
|
+
this.selectedChoice = choices[nextIndex];
|
|
10711
|
+
choices[nextIndex].focus();
|
|
10712
|
+
this.#emitEvents();
|
|
10713
|
+
}
|
|
10714
|
+
}
|
|
10715
|
+
|
|
10716
|
+
#emitEvents() {
|
|
10717
|
+
const val = this.value;
|
|
10718
|
+
this.dispatchEvent(
|
|
10719
|
+
new CustomEvent("input", { detail: val, bubbles: true }),
|
|
10720
|
+
);
|
|
10721
|
+
this.dispatchEvent(
|
|
10722
|
+
new CustomEvent("change", { detail: val, bubbles: true }),
|
|
10723
|
+
);
|
|
10724
|
+
}
|
|
10725
|
+
|
|
10726
|
+
#syncOverflow() {
|
|
10727
|
+
if (this.#overflowMode === "scrollbar") return;
|
|
10728
|
+
const isHorizontal = this.getAttribute("layout") === "horizontal";
|
|
10729
|
+
const threshold = 2;
|
|
10730
|
+
|
|
10731
|
+
if (isHorizontal) {
|
|
10732
|
+
const atStart = this.scrollLeft <= threshold;
|
|
10733
|
+
const atEnd = this.scrollLeft + this.clientWidth >= this.scrollWidth - threshold;
|
|
10734
|
+
this.classList.toggle("overflow-start", !atStart);
|
|
10735
|
+
this.classList.toggle("overflow-end", !atEnd);
|
|
10736
|
+
} else {
|
|
10737
|
+
const atStart = this.scrollTop <= threshold;
|
|
10738
|
+
const atEnd = this.scrollTop + this.clientHeight >= this.scrollHeight - threshold;
|
|
10739
|
+
this.classList.toggle("overflow-start", !atStart);
|
|
10740
|
+
this.classList.toggle("overflow-end", !atEnd);
|
|
10741
|
+
}
|
|
10742
|
+
}
|
|
10743
|
+
|
|
10744
|
+
#startResizeObserver() {
|
|
10745
|
+
this.#resizeObserver?.disconnect();
|
|
10746
|
+
this.#resizeObserver = new ResizeObserver(() => {
|
|
10747
|
+
this.#syncOverflow();
|
|
10748
|
+
});
|
|
10749
|
+
this.#resizeObserver.observe(this);
|
|
10750
|
+
}
|
|
10751
|
+
|
|
10752
|
+
#setupDrag() {
|
|
10753
|
+
if (this.#dragState?.bound) return;
|
|
10754
|
+
if (!this.#dragEnabled) return;
|
|
10755
|
+
|
|
10756
|
+
const onPointerDown = (e) => {
|
|
10757
|
+
if (e.button !== 0) return;
|
|
10758
|
+
const isHorizontal = this.getAttribute("layout") === "horizontal";
|
|
10759
|
+
const hasOverflow = isHorizontal
|
|
10760
|
+
? this.scrollWidth > this.clientWidth
|
|
10761
|
+
: this.scrollHeight > this.clientHeight;
|
|
10762
|
+
if (!hasOverflow) return;
|
|
10763
|
+
|
|
10764
|
+
this.#dragState.active = true;
|
|
10765
|
+
this.#dragState.didDrag = false;
|
|
10766
|
+
this.#dragState.startX = e.clientX;
|
|
10767
|
+
this.#dragState.startY = e.clientY;
|
|
10768
|
+
this.#dragState.scrollLeft = this.scrollLeft;
|
|
10769
|
+
this.#dragState.scrollTop = this.scrollTop;
|
|
10770
|
+
this.style.cursor = "grab";
|
|
10771
|
+
this.style.userSelect = "none";
|
|
10772
|
+
};
|
|
10773
|
+
|
|
10774
|
+
const onPointerMove = (e) => {
|
|
10775
|
+
if (!this.#dragState.active) return;
|
|
10776
|
+
const isHorizontal = this.getAttribute("layout") === "horizontal";
|
|
10777
|
+
const dx = e.clientX - this.#dragState.startX;
|
|
10778
|
+
const dy = e.clientY - this.#dragState.startY;
|
|
10779
|
+
|
|
10780
|
+
if (!this.#dragState.didDrag && Math.abs(isHorizontal ? dx : dy) > 3) {
|
|
10781
|
+
this.#dragState.didDrag = true;
|
|
10782
|
+
this.style.cursor = "grabbing";
|
|
10783
|
+
this.setPointerCapture(e.pointerId);
|
|
10784
|
+
}
|
|
10785
|
+
|
|
10786
|
+
if (!this.#dragState.didDrag) return;
|
|
10787
|
+
|
|
10788
|
+
if (isHorizontal) {
|
|
10789
|
+
this.scrollLeft = this.#dragState.scrollLeft - dx;
|
|
10790
|
+
} else {
|
|
10791
|
+
this.scrollTop = this.#dragState.scrollTop - dy;
|
|
10792
|
+
}
|
|
10793
|
+
};
|
|
10794
|
+
|
|
10795
|
+
const onPointerUp = (e) => {
|
|
10796
|
+
if (!this.#dragState.active) return;
|
|
10797
|
+
const wasDrag = this.#dragState.didDrag;
|
|
10798
|
+
this.#dragState.active = false;
|
|
10799
|
+
this.#dragState.didDrag = false;
|
|
10800
|
+
this.style.cursor = "";
|
|
10801
|
+
this.style.userSelect = "";
|
|
10802
|
+
if (e.pointerId !== undefined) {
|
|
10803
|
+
try { this.releasePointerCapture(e.pointerId); } catch {}
|
|
10804
|
+
}
|
|
10805
|
+
if (wasDrag) {
|
|
10806
|
+
e.preventDefault();
|
|
10807
|
+
e.stopPropagation();
|
|
10808
|
+
}
|
|
10809
|
+
};
|
|
10810
|
+
|
|
10811
|
+
const onClick = (e) => {
|
|
10812
|
+
if (this.#dragState?.suppressClick) {
|
|
10813
|
+
e.stopPropagation();
|
|
10814
|
+
e.preventDefault();
|
|
10815
|
+
this.#dragState.suppressClick = false;
|
|
10816
|
+
}
|
|
10817
|
+
};
|
|
10818
|
+
|
|
10819
|
+
const onPointerUpCapture = (e) => {
|
|
10820
|
+
if (this.#dragState?.didDrag) {
|
|
10821
|
+
this.#dragState.suppressClick = true;
|
|
10822
|
+
setTimeout(() => {
|
|
10823
|
+
if (this.#dragState) this.#dragState.suppressClick = false;
|
|
10824
|
+
}, 0);
|
|
10825
|
+
}
|
|
10826
|
+
};
|
|
10827
|
+
|
|
10828
|
+
this.#dragState = {
|
|
10829
|
+
active: false,
|
|
10830
|
+
didDrag: false,
|
|
10831
|
+
suppressClick: false,
|
|
10832
|
+
startX: 0,
|
|
10833
|
+
startY: 0,
|
|
10834
|
+
scrollLeft: 0,
|
|
10835
|
+
scrollTop: 0,
|
|
10836
|
+
bound: true,
|
|
10837
|
+
onPointerDown,
|
|
10838
|
+
onPointerMove,
|
|
10839
|
+
onPointerUp,
|
|
10840
|
+
onClick,
|
|
10841
|
+
onPointerUpCapture,
|
|
10842
|
+
};
|
|
10843
|
+
|
|
10844
|
+
this.addEventListener("pointerdown", onPointerDown);
|
|
10845
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
10846
|
+
window.addEventListener("pointerup", onPointerUp);
|
|
10847
|
+
this.addEventListener("pointerup", onPointerUpCapture, true);
|
|
10848
|
+
this.addEventListener("click", onClick, true);
|
|
10849
|
+
}
|
|
10850
|
+
|
|
10851
|
+
#teardownDrag() {
|
|
10852
|
+
if (!this.#dragState?.bound) return;
|
|
10853
|
+
this.removeEventListener("pointerdown", this.#dragState.onPointerDown);
|
|
10854
|
+
window.removeEventListener("pointermove", this.#dragState.onPointerMove);
|
|
10855
|
+
window.removeEventListener("pointerup", this.#dragState.onPointerUp);
|
|
10856
|
+
this.removeEventListener("pointerup", this.#dragState.onPointerUpCapture, true);
|
|
10857
|
+
this.removeEventListener("click", this.#dragState.onClick, true);
|
|
10858
|
+
this.style.cursor = "";
|
|
10859
|
+
this.style.userSelect = "";
|
|
10860
|
+
this.#dragState = null;
|
|
10861
|
+
}
|
|
10862
|
+
|
|
10863
|
+
#applyOverflowMode() {
|
|
10864
|
+
if (this.#overflowMode === "scrollbar") {
|
|
10865
|
+
this.#removeNavButtons();
|
|
10866
|
+
} else {
|
|
10867
|
+
this.#createNavButtons();
|
|
10868
|
+
}
|
|
10869
|
+
}
|
|
10870
|
+
|
|
10871
|
+
#removeNavButtons() {
|
|
10872
|
+
this.#navStart?.remove();
|
|
10873
|
+
this.#navEnd?.remove();
|
|
10874
|
+
this.#navStart = null;
|
|
10875
|
+
this.#navEnd = null;
|
|
10876
|
+
this.classList.remove("overflow-start", "overflow-end");
|
|
10877
|
+
}
|
|
10878
|
+
|
|
10879
|
+
#createNavButtons() {
|
|
10880
|
+
if (this.#navStart) return;
|
|
10881
|
+
|
|
10882
|
+
this.#navStart = document.createElement("button");
|
|
10883
|
+
this.#navStart.className = "fig-chooser-nav-start";
|
|
10884
|
+
this.#navStart.setAttribute("tabindex", "-1");
|
|
10885
|
+
this.#navStart.setAttribute("aria-label", "Scroll back");
|
|
10886
|
+
|
|
10887
|
+
this.#navEnd = document.createElement("button");
|
|
10888
|
+
this.#navEnd.className = "fig-chooser-nav-end";
|
|
10889
|
+
this.#navEnd.setAttribute("tabindex", "-1");
|
|
10890
|
+
this.#navEnd.setAttribute("aria-label", "Scroll forward");
|
|
10891
|
+
|
|
10892
|
+
this.#navStart.addEventListener("pointerdown", (e) => {
|
|
10893
|
+
e.stopPropagation();
|
|
10894
|
+
this.#scrollByPage(-1);
|
|
10895
|
+
});
|
|
10896
|
+
|
|
10897
|
+
this.#navEnd.addEventListener("pointerdown", (e) => {
|
|
10898
|
+
e.stopPropagation();
|
|
10899
|
+
this.#scrollByPage(1);
|
|
10900
|
+
});
|
|
10901
|
+
|
|
10902
|
+
this.prepend(this.#navStart);
|
|
10903
|
+
this.append(this.#navEnd);
|
|
10904
|
+
}
|
|
10905
|
+
|
|
10906
|
+
#scrollByPage(direction) {
|
|
10907
|
+
const isHorizontal =
|
|
10908
|
+
this.getAttribute("layout") === "horizontal";
|
|
10909
|
+
const pageSize = isHorizontal ? this.clientWidth : this.clientHeight;
|
|
10910
|
+
const scrollAmount = pageSize * 0.8 * direction;
|
|
10911
|
+
|
|
10912
|
+
this.scrollBy({
|
|
10913
|
+
[isHorizontal ? "left" : "top"]: scrollAmount,
|
|
10914
|
+
behavior: "smooth",
|
|
10915
|
+
});
|
|
10916
|
+
}
|
|
10917
|
+
|
|
10918
|
+
#scrollToChoice(el) {
|
|
10919
|
+
if (!el) return;
|
|
10920
|
+
requestAnimationFrame(() => {
|
|
10921
|
+
const overflowY = this.scrollHeight > this.clientHeight;
|
|
10922
|
+
const overflowX = this.scrollWidth > this.clientWidth;
|
|
10923
|
+
if (!overflowX && !overflowY) return;
|
|
10924
|
+
|
|
10925
|
+
const options = { behavior: "smooth" };
|
|
10926
|
+
|
|
10927
|
+
if (overflowY) {
|
|
10928
|
+
const target = el.offsetTop - this.clientHeight / 2 + el.offsetHeight / 2;
|
|
10929
|
+
options.top = target;
|
|
10930
|
+
}
|
|
10931
|
+
|
|
10932
|
+
if (overflowX) {
|
|
10933
|
+
const target = el.offsetLeft - this.clientWidth / 2 + el.offsetWidth / 2;
|
|
10934
|
+
options.left = target;
|
|
10935
|
+
}
|
|
10936
|
+
|
|
10937
|
+
this.scrollTo(options);
|
|
10938
|
+
});
|
|
10939
|
+
}
|
|
10940
|
+
|
|
10941
|
+
#startObserver() {
|
|
10942
|
+
this.#mutationObserver?.disconnect();
|
|
10943
|
+
this.#mutationObserver = new MutationObserver(() => {
|
|
10944
|
+
const choices = this.choices;
|
|
10945
|
+
if (this.#selectedChoice && !choices.includes(this.#selectedChoice)) {
|
|
10946
|
+
this.#selectedChoice = null;
|
|
10947
|
+
this.#syncSelection();
|
|
10948
|
+
} else if (!this.#selectedChoice && choices.length) {
|
|
10949
|
+
this.#syncSelection();
|
|
10950
|
+
}
|
|
10951
|
+
});
|
|
10952
|
+
this.#mutationObserver.observe(this, { childList: true, subtree: true });
|
|
10953
|
+
}
|
|
10954
|
+
}
|
|
10955
|
+
customElements.define("fig-chooser", FigChooser);
|