@unabridged/midwest 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +5 -5
  2. package/app/assets/icons/available-icons.yml +11 -0
  3. package/app/assets/icons/loader-audio.svg +29 -0
  4. package/app/assets/icons/loader-ball-triangle.svg +46 -0
  5. package/app/assets/icons/loader-bars.svg +52 -0
  6. package/app/assets/icons/loader-circles.svg +20 -0
  7. package/app/assets/icons/loader-grid.svg +56 -0
  8. package/app/assets/icons/loader-oval.svg +17 -0
  9. package/app/assets/icons/loader-puff.svg +37 -0
  10. package/app/assets/icons/loader-rings.svg +42 -0
  11. package/app/assets/icons/loader-spinning-circles.svg +55 -0
  12. package/app/assets/icons/loader-tail-spin.svg +32 -0
  13. package/app/assets/icons/loader-three-dots.svg +33 -0
  14. package/app/assets/javascript/midwest/index.ts +2 -10
  15. package/app/assets/javascript/midwest.js +365 -81
  16. package/app/assets/javascript/midwest.js.map +1 -1
  17. package/app/assets/stylesheets/midwest/color-system.css +0 -2
  18. package/app/assets/stylesheets/midwest.css +1 -1
  19. package/app/assets/stylesheets/midwest.tailwind.css +3 -2
  20. package/dist/css/midwest.css +1 -1
  21. package/dist/javascript/collection/app/assets/javascript/midwest/index.js +2 -10
  22. package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
  23. package/dist/javascript/collection/app/components/midwest/card_component/card_component_controller.js +10 -2
  24. package/dist/javascript/collection/app/components/midwest/card_component/card_component_controller.js.map +1 -1
  25. package/dist/javascript/collection/app/components/midwest/carousel_component/carousel_component_controller.js +62 -2
  26. package/dist/javascript/collection/app/components/midwest/carousel_component/carousel_component_controller.js.map +1 -1
  27. package/dist/javascript/collection/app/components/midwest/dialog_component/dialog_component_controller.js +58 -17
  28. package/dist/javascript/collection/app/components/midwest/dialog_component/dialog_component_controller.js.map +1 -1
  29. package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js +56 -1
  30. package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js.map +1 -1
  31. package/dist/javascript/collection/app/components/midwest/form/file_component/file_component_controller.js +49 -22
  32. package/dist/javascript/collection/app/components/midwest/form/file_component/file_component_controller.js.map +1 -1
  33. package/dist/javascript/collection/app/components/midwest/form/form_controller.js +36 -0
  34. package/dist/javascript/collection/app/components/midwest/form/form_controller.js.map +1 -0
  35. package/dist/javascript/collection/app/components/midwest/form/input_component/input_component_controller.js +48 -0
  36. package/dist/javascript/collection/app/components/midwest/form/input_component/input_component_controller.js.map +1 -1
  37. package/dist/javascript/collection/app/components/midwest/prose_component/prose_component_controller.js +2 -1
  38. package/dist/javascript/collection/app/components/midwest/prose_component/prose_component_controller.js.map +1 -1
  39. package/dist/javascript/collection/app/components/midwest/tabs_component/tabs_component_controller.js +32 -0
  40. package/dist/javascript/collection/app/components/midwest/tabs_component/tabs_component_controller.js.map +1 -1
  41. package/dist/javascript/collection/app/components/midwest/tooltip_component/tooltip_component_controller.js +15 -1
  42. package/dist/javascript/collection/app/components/midwest/tooltip_component/tooltip_component_controller.js.map +1 -1
  43. package/dist/javascript/midwest.d.ts +2 -0
  44. package/dist/javascript/midwest.js +365 -81
  45. package/dist/javascript/midwest.js.map +1 -1
  46. package/package.json +2 -1
@@ -494,9 +494,16 @@ class Card extends Controller {
494
494
  originalHeight: { type: Number, default: 0 },
495
495
  backHeight: { type: Number, default: 0 }
496
496
  };
497
+ resizeHandler = null;
497
498
  connect() {
498
499
  this.applyReady();
499
500
  }
501
+ disconnect() {
502
+ if (this.resizeHandler) {
503
+ window.removeEventListener("resize", this.resizeHandler);
504
+ this.resizeHandler = null;
505
+ }
506
+ }
500
507
  get flippable() {
501
508
  return this.element.classList.contains("flippable");
502
509
  }
@@ -505,10 +512,11 @@ class Card extends Controller {
505
512
  this.originalHeightValue = this.frontTarget.offsetHeight;
506
513
  this.backHeightValue = this.backTarget.offsetHeight;
507
514
  this.element.classList.add("flip-ready");
508
- window.addEventListener("resize", () => {
515
+ this.resizeHandler = () => {
509
516
  this.originalHeightValue = this.frontTarget.offsetHeight;
510
517
  this.backHeightValue = this.backTarget.offsetHeight;
511
- });
518
+ };
519
+ window.addEventListener("resize", this.resizeHandler);
512
520
  }
513
521
  }
514
522
  originalHeightValueChanged() {
@@ -535,16 +543,18 @@ class Banner extends Controller {
535
543
  }
536
544
  }
537
545
 
538
- class Callout extends Controller {
539
- connect() {
540
- console.log("Hello from Callout!");
541
- }
542
- }
543
-
544
546
  class Input extends Controller {
545
547
  static targets = ["input"];
548
+ updateClearButton = null;
546
549
  connect() {
547
550
  this.applyColor();
551
+ this.applySearchClear();
552
+ }
553
+ disconnect() {
554
+ if (this.updateClearButton) {
555
+ this.inputTarget.removeEventListener("input", this.updateClearButton);
556
+ this.updateClearButton = null;
557
+ }
548
558
  }
549
559
  applyColor() {
550
560
  if (this.inputTarget.type !== "color") {
@@ -554,36 +564,103 @@ class Input extends Controller {
554
564
  this.inputTarget.setAttribute("value", this.inputTarget.value);
555
565
  });
556
566
  }
567
+ applySearchClear() {
568
+ if (this.inputTarget.type !== "search")
569
+ return;
570
+ if (CSS.supports("selector(::-webkit-search-cancel-button)"))
571
+ return;
572
+ const button = document.createElement("button");
573
+ button.type = "button";
574
+ button.setAttribute("aria-label", "Clear search");
575
+ button.classList.add("midwest-input-search-clear");
576
+ button.hidden = this.inputTarget.value.length === 0;
577
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
578
+ svg.setAttribute("viewBox", "0 0 24 24");
579
+ svg.setAttribute("fill", "none");
580
+ svg.setAttribute("stroke", "currentColor");
581
+ svg.setAttribute("stroke-width", "2");
582
+ svg.setAttribute("aria-hidden", "true");
583
+ svg.setAttribute("width", "16");
584
+ svg.setAttribute("height", "16");
585
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
586
+ path.setAttribute("stroke-linecap", "round");
587
+ path.setAttribute("stroke-linejoin", "round");
588
+ path.setAttribute("d", "M6 18L18 6M6 6l12 12");
589
+ svg.appendChild(path);
590
+ button.appendChild(svg);
591
+ const wrapper = document.createElement("div");
592
+ wrapper.classList.add("midwest-input-search-wrapper");
593
+ this.inputTarget.parentElement?.insertBefore(wrapper, this.inputTarget);
594
+ wrapper.appendChild(this.inputTarget);
595
+ wrapper.appendChild(button);
596
+ this.updateClearButton = () => {
597
+ button.hidden = this.inputTarget.value.length === 0;
598
+ };
599
+ this.inputTarget.addEventListener("input", this.updateClearButton);
600
+ button.addEventListener("click", () => {
601
+ this.inputTarget.value = "";
602
+ button.hidden = true;
603
+ this.inputTarget.dispatchEvent(new Event("input", { bubbles: true }));
604
+ this.inputTarget.focus();
605
+ });
606
+ }
557
607
  }
558
608
 
559
609
  class File extends Controller {
560
610
  static targets = ["input", "output"];
611
+ // Arrow function so `this` is stable for add/removeEventListener pairing.
612
+ handleChange = () => {
613
+ this.renderPreviews();
614
+ };
561
615
  connect() {
562
- this.inputTarget.addEventListener("change", () => {
563
- console.log("words words words");
564
- this.outputTarget.innerHTML = this.previewHTML;
616
+ this.inputTarget.addEventListener("change", this.handleChange);
617
+ }
618
+ disconnect() {
619
+ this.inputTarget.removeEventListener("change", this.handleChange);
620
+ }
621
+ renderPreviews() {
622
+ this.outputTarget.innerHTML = "";
623
+ this.files.forEach((file) => {
624
+ this.outputTarget.appendChild(this.buildPreview(file));
565
625
  });
566
626
  }
567
- get previewHTML() {
568
- return this.files.map(
569
- (file) => `<div class="file-preview file-preview-${file.type} layer-children">
570
- <span class='midwest-badge size-default theme-primary'><span class='value'>${file.type}</span></span>
571
- <span class='midwest-badge size-default theme-gray ml-auto'><span class='value'>${new Intl.NumberFormat(
572
- void 0,
573
- {
574
- notation: "compact",
575
- style: "unit",
576
- unit: "byte",
577
- unitDisplay: "narrow"
578
- }
579
- ).format(file.size)}</span></span>
580
-
581
- ${file.type.includes("image") ? `<img src="${window.URL.createObjectURL(file)}" />` : ""}
582
- ${!file.type.includes("image") ? `<div class="placeholder-file">${file.name}</div>` : ""}
583
-
584
- </div>`
585
- ).join("");
627
+ buildPreview(file) {
628
+ const safeType = file.type ? file.type.replace("/", "-") : "unknown";
629
+ const div = document.createElement("div");
630
+ div.className = `file-preview file-preview-${safeType} layer-children`;
631
+ const typeBadge = document.createElement("span");
632
+ typeBadge.className = "midwest-badge size-default theme-primary";
633
+ const typeValue = document.createElement("span");
634
+ typeValue.className = "value";
635
+ typeValue.textContent = file.type || "Unknown";
636
+ typeBadge.appendChild(typeValue);
637
+ div.appendChild(typeBadge);
638
+ const sizeBadge = document.createElement("span");
639
+ sizeBadge.className = "midwest-badge size-default theme-gray ml-auto";
640
+ const sizeValue = document.createElement("span");
641
+ sizeValue.className = "value";
642
+ sizeValue.textContent = new Intl.NumberFormat(void 0, {
643
+ notation: "compact",
644
+ style: "unit",
645
+ unit: "byte",
646
+ unitDisplay: "narrow"
647
+ }).format(file.size);
648
+ sizeBadge.appendChild(sizeValue);
649
+ div.appendChild(sizeBadge);
650
+ if (file.type.startsWith("image/")) {
651
+ const img = document.createElement("img");
652
+ img.src = window.URL.createObjectURL(file);
653
+ div.appendChild(img);
654
+ } else {
655
+ const placeholder = document.createElement("div");
656
+ placeholder.className = "placeholder-file";
657
+ placeholder.innerHTML = file.type.startsWith("video/") ? this.videoIcon : this.fileIcon;
658
+ div.appendChild(placeholder);
659
+ }
660
+ return div;
586
661
  }
662
+ fileIcon = '<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512" aria-hidden="true"><path d="M416 221.25V416a48 48 0 01-48 48H144a48 48 0 01-48-48V96a48 48 0 0148-48h98.75a32 32 0 0122.62 9.37l141.26 141.26a32 32 0 019.37 22.62z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><path d="M256 56v120a32 32 0 0032 32h120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>';
663
+ videoIcon = '<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512" aria-hidden="true"><path d="M374.79 308.78L457.5 367a16 16 0 0022.5-14.62V159.62A16 16 0 00457.5 145l-82.71 58.22A16 16 0 00368 216.3v79.4a16 16 0 006.79 13.08z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M268 384H84a52.15 52.15 0 01-52-52V180a52.15 52.15 0 0152-52h184.48A51.68 51.68 0 01320 179.52V332a52.15 52.15 0 01-52 52z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/></svg>';
587
664
  get files() {
588
665
  if (this.inputTarget.files == null) {
589
666
  return [];
@@ -592,12 +669,6 @@ class File extends Controller {
592
669
  }
593
670
  }
594
671
 
595
- class Switch extends Controller {
596
- connect() {
597
- console.log("Hello from Switch!");
598
- }
599
- }
600
-
601
672
  /**
602
673
  * Custom positioning reference element.
603
674
  * @see https://floating-ui.com/docs/virtual-elements
@@ -2740,7 +2811,9 @@ class Dropdown extends Controller {
2740
2811
  }
2741
2812
  this.buttonEl.removeEventListener("click", this.unsupportedClickCB);
2742
2813
  this.element.removeEventListener("toggle", this.addOpenedClass);
2814
+ this.element.removeEventListener("keydown", this.handleKeydown);
2743
2815
  this.element.removeEventListener("turbo:click", this.closeIfOpen);
2816
+ this.element.removeEventListener("popover:close", this.closeIfOpen);
2744
2817
  }
2745
2818
  unsupportedClickCB = (event) => {
2746
2819
  event.preventDefault();
@@ -2756,8 +2829,61 @@ class Dropdown extends Controller {
2756
2829
  return this.computedValue || !this.supportedValue;
2757
2830
  }
2758
2831
  addOpenedClass = (event) => {
2759
- this.buttonEl.classList.toggle("opened", event.newState === "open");
2832
+ const isOpen = event.newState === "open";
2833
+ this.buttonEl.classList.toggle("opened", isOpen);
2834
+ this.buttonEl.setAttribute("aria-expanded", String(isOpen));
2835
+ if (isOpen) {
2836
+ this.element.addEventListener("keydown", this.handleKeydown);
2837
+ requestAnimationFrame(() => this.menuItems[0]?.focus());
2838
+ } else {
2839
+ this.element.removeEventListener("keydown", this.handleKeydown);
2840
+ }
2841
+ };
2842
+ handleKeydown = (event) => {
2843
+ const items = this.menuItems;
2844
+ if (items.length === 0)
2845
+ return;
2846
+ const currentIndex = items.findIndex((item) => item === document.activeElement);
2847
+ switch (event.key) {
2848
+ case "ArrowDown": {
2849
+ event.preventDefault();
2850
+ const next = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
2851
+ items[next].focus();
2852
+ break;
2853
+ }
2854
+ case "ArrowUp": {
2855
+ event.preventDefault();
2856
+ const prev = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
2857
+ items[prev].focus();
2858
+ break;
2859
+ }
2860
+ case "Home":
2861
+ event.preventDefault();
2862
+ items[0].focus();
2863
+ break;
2864
+ case "End":
2865
+ event.preventDefault();
2866
+ items[items.length - 1].focus();
2867
+ break;
2868
+ case "Escape":
2869
+ event.preventDefault();
2870
+ this.element.hidePopover();
2871
+ this.buttonEl.focus();
2872
+ break;
2873
+ case "Tab":
2874
+ this.element.hidePopover();
2875
+ break;
2876
+ default:
2877
+ return;
2878
+ }
2760
2879
  };
2880
+ get menuItems() {
2881
+ return Array.from(
2882
+ this.element.querySelectorAll(
2883
+ '[role="menuitem"]:not([disabled]):not([aria-disabled="true"])'
2884
+ )
2885
+ );
2886
+ }
2761
2887
  computedCB(entries) {
2762
2888
  entries.forEach(async () => {
2763
2889
  this.computation();
@@ -2801,12 +2927,6 @@ class Dropdown extends Controller {
2801
2927
  }
2802
2928
  }
2803
2929
 
2804
- class Avatar extends Controller {
2805
- connect() {
2806
- console.log("Hello from Avatar!");
2807
- }
2808
- }
2809
-
2810
2930
  class Prose extends Controller {
2811
2931
  static values = {
2812
2932
  initial: { type: Number, default: 1 },
@@ -2816,7 +2936,8 @@ class Prose extends Controller {
2816
2936
  if (!this.element.classList.contains("clamp")) {
2817
2937
  return;
2818
2938
  }
2819
- this.initialValue = parseInt(getComputedStyle(this.element).getPropertyValue("--clamp"));
2939
+ const clamp = parseInt(getComputedStyle(this.element).getPropertyValue("--clamp"), 10);
2940
+ this.initialValue = isNaN(clamp) ? 1 : clamp;
2820
2941
  this.element.addEventListener("click", () => {
2821
2942
  this.toggle();
2822
2943
  });
@@ -2836,9 +2957,19 @@ class Dialog extends Controller {
2836
2957
  static values = {
2837
2958
  resetFrame: { type: Boolean, default: false }
2838
2959
  };
2960
+ _previousFocus = null;
2839
2961
  connect() {
2840
- document.addEventListener("turbo:frame-load", this.setupDialogListeners.bind(this));
2841
- this.setupDialogListeners();
2962
+ document.addEventListener("turbo:frame-load", this.bindToggleEls);
2963
+ document.addEventListener("turbo:before-visit", this.dismiss);
2964
+ document.addEventListener("click", this.handleClickOutside);
2965
+ this.dialog.addEventListener("close", this.onClose);
2966
+ this.bindToggleEls();
2967
+ }
2968
+ disconnect() {
2969
+ document.removeEventListener("turbo:frame-load", this.bindToggleEls);
2970
+ document.removeEventListener("turbo:before-visit", this.dismiss);
2971
+ document.removeEventListener("click", this.handleClickOutside);
2972
+ this.dialog.removeEventListener("close", this.onClose);
2842
2973
  }
2843
2974
  get dialog() {
2844
2975
  return this.element;
@@ -2852,20 +2983,18 @@ class Dialog extends Controller {
2852
2983
  get frameEl() {
2853
2984
  return this.dialog.querySelector("turbo-frame");
2854
2985
  }
2855
- setupDialogListeners() {
2856
- document.addEventListener("click", (event) => {
2857
- if (event?.target instanceof Element) {
2858
- if (!event?.target?.contains(this.dialog))
2859
- return;
2860
- this.dismiss();
2861
- }
2862
- });
2863
- document.addEventListener("turbo:before-visit", this.dismiss.bind(this));
2986
+ // Arrow function so `this` is stable for add/removeEventListener pairing.
2987
+ handleClickOutside = (event) => {
2988
+ if (event.target !== this.dialog)
2989
+ return;
2990
+ this.dismiss();
2991
+ };
2992
+ // Arrow function so `this` is stable for add/removeEventListener pairing.
2993
+ bindToggleEls = () => {
2864
2994
  this.toggleEls.forEach((el) => {
2865
2995
  el.addEventListener("click", this.toggle.bind(this));
2866
2996
  });
2867
- this.dialog.addEventListener("close", this.onClose.bind(this));
2868
- }
2997
+ };
2869
2998
  remoteContentTargetConnected() {
2870
2999
  this.show();
2871
3000
  }
@@ -2876,32 +3005,65 @@ class Dialog extends Controller {
2876
3005
  if (this.dialog.open) {
2877
3006
  this.dialog.close();
2878
3007
  } else {
3008
+ this._previousFocus = document.activeElement;
2879
3009
  this.dialog.showModal();
3010
+ this.dialog.addEventListener("keydown", this.handleFocusTrap);
2880
3011
  }
2881
3012
  }
2882
3013
  show(e = void 0) {
2883
3014
  if (e instanceof Event)
2884
3015
  e.preventDefault();
2885
3016
  this.htmlEl.classList.toggle("overflow-hidden", true);
3017
+ this._previousFocus = document.activeElement;
2886
3018
  this.dialog.showModal();
3019
+ this.dialog.addEventListener("keydown", this.handleFocusTrap);
2887
3020
  }
2888
3021
  hide({ detail }) {
2889
3022
  if (!detail.success)
2890
3023
  return;
2891
3024
  this.dismiss();
2892
3025
  }
2893
- dismiss() {
3026
+ // Arrow function so `this` is stable for add/removeEventListener pairing.
3027
+ dismiss = () => {
2894
3028
  this.dialog.close();
2895
- }
3029
+ };
2896
3030
  reloadPage() {
2897
3031
  window.location.reload();
2898
3032
  }
2899
- async onClose() {
3033
+ handleFocusTrap = (event) => {
3034
+ if (event.key !== "Tab")
3035
+ return;
3036
+ const focusable = Array.from(
3037
+ this.dialog.querySelectorAll(
3038
+ 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
3039
+ )
3040
+ ).filter((el) => !el.closest("[inert]"));
3041
+ if (focusable.length === 0)
3042
+ return;
3043
+ const first = focusable[0];
3044
+ const last = focusable[focusable.length - 1];
3045
+ if (event.shiftKey) {
3046
+ if (document.activeElement === first) {
3047
+ event.preventDefault();
3048
+ last.focus();
3049
+ }
3050
+ } else {
3051
+ if (document.activeElement === last) {
3052
+ event.preventDefault();
3053
+ first.focus();
3054
+ }
3055
+ }
3056
+ };
3057
+ // Arrow function so `this` is stable for add/removeEventListener pairing.
3058
+ onClose = async () => {
3059
+ this.dialog.removeEventListener("keydown", this.handleFocusTrap);
3060
+ this._previousFocus?.focus();
3061
+ this._previousFocus = null;
2900
3062
  this.htmlEl.classList.toggle("overflow-hidden", false);
2901
3063
  await this.delay(500);
2902
3064
  this.resetForm();
2903
3065
  this.resetFrame();
2904
- }
3066
+ };
2905
3067
  resetFrame() {
2906
3068
  if (!this.resetFrameValue || this.frameEl == null)
2907
3069
  return;
@@ -2920,18 +3082,6 @@ class Dialog extends Controller {
2920
3082
  }
2921
3083
  }
2922
3084
 
2923
- class Video extends Controller {
2924
- connect() {
2925
- console.log("Hello from Video!");
2926
- }
2927
- }
2928
-
2929
- class DropdownItem extends Controller {
2930
- connect() {
2931
- console.log("Hello from DropdownItem!");
2932
- }
2933
- }
2934
-
2935
3085
  class Tabs extends Controller {
2936
3086
  static targets = ["tab", "panel"];
2937
3087
  connect() {
@@ -2939,6 +3089,10 @@ class Tabs extends Controller {
2939
3089
  (tab) => tab.getAttribute("aria-selected") === "true"
2940
3090
  );
2941
3091
  this.activateTab(activeIndex >= 0 ? activeIndex : 0);
3092
+ this.element.addEventListener("keydown", this.handleKeydown);
3093
+ }
3094
+ disconnect() {
3095
+ this.element.removeEventListener("keydown", this.handleKeydown);
2942
3096
  }
2943
3097
  select(event) {
2944
3098
  const tab = event.currentTarget;
@@ -2946,6 +3100,34 @@ class Tabs extends Controller {
2946
3100
  if (index >= 0)
2947
3101
  this.activateTab(index);
2948
3102
  }
3103
+ handleKeydown = (event) => {
3104
+ const tabs = this.tabTargets;
3105
+ if (tabs.length === 0)
3106
+ return;
3107
+ const currentIndex = tabs.findIndex((t) => t === document.activeElement);
3108
+ if (currentIndex < 0)
3109
+ return;
3110
+ let nextIndex;
3111
+ switch (event.key) {
3112
+ case "ArrowLeft":
3113
+ nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
3114
+ break;
3115
+ case "ArrowRight":
3116
+ nextIndex = (currentIndex + 1) % tabs.length;
3117
+ break;
3118
+ case "Home":
3119
+ nextIndex = 0;
3120
+ break;
3121
+ case "End":
3122
+ nextIndex = tabs.length - 1;
3123
+ break;
3124
+ default:
3125
+ return;
3126
+ }
3127
+ event.preventDefault();
3128
+ this.activateTab(nextIndex);
3129
+ tabs[nextIndex].focus();
3130
+ };
2949
3131
  activateTab(index) {
2950
3132
  this.tabTargets.forEach((tab, i) => {
2951
3133
  const active = i === index;
@@ -2965,12 +3147,26 @@ class Tabs extends Controller {
2965
3147
 
2966
3148
  class Tooltip extends Controller {
2967
3149
  static targets = ["popover"];
3150
+ connect() {
3151
+ document.addEventListener("keydown", this.handleKeydown);
3152
+ }
3153
+ disconnect() {
3154
+ document.removeEventListener("keydown", this.handleKeydown);
3155
+ }
2968
3156
  show() {
2969
3157
  this.popoverTarget.showPopover();
2970
3158
  }
2971
3159
  hide() {
2972
- this.popoverTarget.hidePopover();
3160
+ try {
3161
+ this.popoverTarget.hidePopover();
3162
+ } catch {
3163
+ }
2973
3164
  }
3165
+ handleKeydown = (event) => {
3166
+ if (event.key !== "Escape")
3167
+ return;
3168
+ this.hide();
3169
+ };
2974
3170
  }
2975
3171
 
2976
3172
  const NON_SUBMITTING_INPUT_TYPES = /* @__PURE__ */ new Set([
@@ -2993,16 +3189,24 @@ class Carousel extends Controller {
2993
3189
  observer = null;
2994
3190
  _currentIndex = 0;
2995
3191
  _maxUnlocked = 0;
3192
+ _lastTouchX = 0;
2996
3193
  connect() {
2997
3194
  this.setupObserver();
2998
3195
  this.updateNavigation();
2999
3196
  if (this.wizardValue) {
3000
3197
  this.element.addEventListener("keydown", this.handleKeydown);
3198
+ this.trackTarget.addEventListener("wheel", this.handleWheel, { passive: false });
3199
+ this.trackTarget.addEventListener("touchstart", this.handleTouchStart, { passive: true });
3200
+ this.trackTarget.addEventListener("touchmove", this.handleTouchMove, { passive: false });
3201
+ this.updateSlideAccessibility();
3001
3202
  }
3002
3203
  }
3003
3204
  disconnect() {
3004
3205
  this.observer?.disconnect();
3005
3206
  this.element.removeEventListener("keydown", this.handleKeydown);
3207
+ this.trackTarget.removeEventListener("wheel", this.handleWheel);
3208
+ this.trackTarget.removeEventListener("touchstart", this.handleTouchStart);
3209
+ this.trackTarget.removeEventListener("touchmove", this.handleTouchMove);
3006
3210
  }
3007
3211
  next() {
3008
3212
  this.trackTarget.scrollBy({ left: this.trackTarget.clientWidth, behavior: "smooth" });
@@ -3027,10 +3231,13 @@ class Carousel extends Controller {
3027
3231
  const nextIndex = this._currentIndex + 1;
3028
3232
  if (nextIndex > this._maxUnlocked) {
3029
3233
  this._maxUnlocked = nextIndex;
3234
+ this.updateSlideAccessibility();
3030
3235
  }
3031
3236
  this.next();
3032
3237
  }
3033
3238
  scrolled() {
3239
+ if (this.wizardValue)
3240
+ this.clampToFrontier();
3034
3241
  this.updateCurrentIndex();
3035
3242
  this.updateNavigation();
3036
3243
  }
@@ -3048,9 +3255,49 @@ class Carousel extends Controller {
3048
3255
  event.preventDefault();
3049
3256
  this.unlockNext();
3050
3257
  };
3258
+ // Blocks wheel-based horizontal scrolling past the wizard frontier.
3259
+ handleWheel = (event) => {
3260
+ const maxLeft = this._maxUnlocked * this.trackTarget.clientWidth;
3261
+ if (event.deltaX > 0 && this.trackTarget.scrollLeft >= maxLeft - 2) {
3262
+ event.preventDefault();
3263
+ }
3264
+ };
3265
+ handleTouchStart = (event) => {
3266
+ this._lastTouchX = event.touches[0].clientX;
3267
+ };
3268
+ // Blocks touch-swipe scrolling past the wizard frontier. Tracks incremental
3269
+ // delta per move event so direction is accurate mid-gesture.
3270
+ handleTouchMove = (event) => {
3271
+ const currentX = event.touches[0].clientX;
3272
+ const delta = currentX - this._lastTouchX;
3273
+ this._lastTouchX = currentX;
3274
+ const maxLeft = this._maxUnlocked * this.trackTarget.clientWidth;
3275
+ if (delta < 0 && this.trackTarget.scrollLeft >= maxLeft - 2) {
3276
+ event.preventDefault();
3277
+ }
3278
+ };
3051
3279
  get slides() {
3052
3280
  return Array.from(this.trackTarget.children);
3053
3281
  }
3282
+ // Snaps scroll position back to the frontier if the user somehow scrolled
3283
+ // past it (e.g. momentum after a partially-blocked gesture).
3284
+ clampToFrontier() {
3285
+ const maxLeft = this._maxUnlocked * this.trackTarget.clientWidth;
3286
+ if (this.trackTarget.scrollLeft > maxLeft + 2) {
3287
+ this.trackTarget.scrollLeft = maxLeft;
3288
+ }
3289
+ }
3290
+ // Applies/removes `inert` on each slide based on the wizard frontier,
3291
+ // removing locked slides from the tab order and accessibility tree.
3292
+ updateSlideAccessibility() {
3293
+ this.slides.forEach((slide, i) => {
3294
+ if (i > this._maxUnlocked) {
3295
+ slide.setAttribute("inert", "");
3296
+ } else {
3297
+ slide.removeAttribute("inert");
3298
+ }
3299
+ });
3300
+ }
3054
3301
  updateCurrentIndex() {
3055
3302
  const track = this.trackTarget;
3056
3303
  if (track.clientWidth > 0) {
@@ -3067,7 +3314,15 @@ class Carousel extends Controller {
3067
3314
  entries.forEach((entry) => {
3068
3315
  const index = this.slides.indexOf(entry.target);
3069
3316
  if (index >= 0) {
3070
- this.dotTargets[index]?.classList.toggle("active", entry.isIntersecting);
3317
+ const dot = this.dotTargets[index];
3318
+ if (!dot)
3319
+ return;
3320
+ dot.classList.toggle("active", entry.isIntersecting);
3321
+ if (entry.isIntersecting) {
3322
+ dot.setAttribute("aria-current", "true");
3323
+ } else {
3324
+ dot.removeAttribute("aria-current");
3325
+ }
3071
3326
  }
3072
3327
  });
3073
3328
  },
@@ -3082,8 +3337,9 @@ class Carousel extends Controller {
3082
3337
  if (this.hasPrevTarget)
3083
3338
  this.prevTarget.classList.toggle("hide", atStart);
3084
3339
  if (this.hasNextTarget) {
3340
+ const atLastSlide = this._currentIndex >= this.slides.length - 1;
3085
3341
  const frontierReached = this.wizardValue && this._currentIndex >= this._maxUnlocked;
3086
- this.nextTarget.classList.toggle("hide", atEnd || frontierReached);
3342
+ this.nextTarget.classList.toggle("hide", atEnd || atLastSlide || frontierReached);
3087
3343
  }
3088
3344
  if (this.wizardValue && this.hasDotTarget) {
3089
3345
  this.dotTargets.forEach((dot, i) => {
@@ -3224,23 +3480,51 @@ class FormLiveSummary extends Controller {
3224
3480
  }
3225
3481
  }
3226
3482
 
3483
+ class Form extends Controller {
3484
+ connect() {
3485
+ this.element.addEventListener("submit", this.handleSubmit);
3486
+ this.element.addEventListener("turbo:submit-end", this.handleTurboEnd);
3487
+ }
3488
+ disconnect() {
3489
+ this.element.removeEventListener("submit", this.handleSubmit);
3490
+ this.element.removeEventListener("turbo:submit-end", this.handleTurboEnd);
3491
+ }
3492
+ handleSubmit = () => {
3493
+ this.submitButtons.forEach((btn) => {
3494
+ btn.classList.add("loading");
3495
+ btn.disabled = true;
3496
+ });
3497
+ };
3498
+ // Restore buttons if the submission fails (e.g. a Turbo Frame validation
3499
+ // error that re-renders the form without a full page transition).
3500
+ handleTurboEnd = (event) => {
3501
+ if (!event.detail?.success) {
3502
+ this.submitButtons.forEach((btn) => {
3503
+ btn.classList.remove("loading");
3504
+ btn.disabled = false;
3505
+ });
3506
+ }
3507
+ };
3508
+ get submitButtons() {
3509
+ return Array.from(
3510
+ this.element.querySelectorAll('button[type="submit"], button:not([type])')
3511
+ );
3512
+ }
3513
+ }
3514
+
3227
3515
  function registerMidwestControllers(application) {
3228
3516
  application.register("midwest-card", Card);
3229
3517
  application.register("midwest-banner", Banner);
3230
- application.register("midwest-callout", Callout);
3231
3518
  application.register("midwest-input", Input);
3232
3519
  application.register("midwest-file", File);
3233
- application.register("midwest-switch", Switch);
3234
3520
  application.register("midwest-dropdown", Dropdown);
3235
- application.register("midwest-avatar", Avatar);
3236
3521
  application.register("midwest-prose", Prose);
3237
3522
  application.register("midwest-dialog", Dialog);
3238
- application.register("midwest-video", Video);
3239
- application.register("midwest-dropdown-item", DropdownItem);
3240
3523
  application.register("midwest-tabs", Tabs);
3241
3524
  application.register("midwest-tooltip", Tooltip);
3242
3525
  application.register("midwest-carousel", Carousel);
3243
3526
  application.register("midwest-form-live-summary", FormLiveSummary);
3527
+ application.register("midwest-form", Form);
3244
3528
  }
3245
3529
 
3246
3530
  export { Banner, Card, registerMidwestControllers };