@unabridged/midwest 0.8.2 → 0.10.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 +371 -82
  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 +4 -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 +62 -2
  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 +371 -82
  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();
@@ -2749,15 +2822,73 @@ class Dropdown extends Controller {
2749
2822
  };
2750
2823
  get buttonEl() {
2751
2824
  return this.element.parentElement?.querySelector(
2752
- `[popovertarget="${this.element.id}"]`
2825
+ `[popovertarget="${this.element.id}"]:not([popovertargetaction="hide"])`
2753
2826
  );
2754
2827
  }
2755
2828
  get isComputed() {
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
+ if (!["ArrowDown", "ArrowUp", "Home", "End", "Escape", "Tab"].includes(event.key))
2847
+ return;
2848
+ event.stopPropagation();
2849
+ const currentIndex = items.findIndex((item) => item === document.activeElement);
2850
+ switch (event.key) {
2851
+ case "ArrowDown": {
2852
+ event.preventDefault();
2853
+ const next = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
2854
+ items[next].focus();
2855
+ break;
2856
+ }
2857
+ case "ArrowUp": {
2858
+ event.preventDefault();
2859
+ const prev = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
2860
+ items[prev].focus();
2861
+ break;
2862
+ }
2863
+ case "Home":
2864
+ event.preventDefault();
2865
+ items[0].focus();
2866
+ break;
2867
+ case "End":
2868
+ event.preventDefault();
2869
+ items[items.length - 1].focus();
2870
+ break;
2871
+ case "Escape":
2872
+ event.preventDefault();
2873
+ this.element.hidePopover();
2874
+ requestAnimationFrame(() => this.buttonEl?.focus());
2875
+ break;
2876
+ case "Tab":
2877
+ this.element.hidePopover();
2878
+ break;
2879
+ default:
2880
+ return;
2881
+ }
2760
2882
  };
2883
+ get menuItems() {
2884
+ return Array.from(
2885
+ this.element.querySelectorAll(
2886
+ '[role="menuitem"]:not([disabled]):not([aria-disabled="true"])'
2887
+ )
2888
+ ).filter(
2889
+ (item) => item.closest('[data-controller~="midwest-dropdown"]') === this.element && getComputedStyle(item).display !== "none"
2890
+ );
2891
+ }
2761
2892
  computedCB(entries) {
2762
2893
  entries.forEach(async () => {
2763
2894
  this.computation();
@@ -2801,12 +2932,6 @@ class Dropdown extends Controller {
2801
2932
  }
2802
2933
  }
2803
2934
 
2804
- class Avatar extends Controller {
2805
- connect() {
2806
- console.log("Hello from Avatar!");
2807
- }
2808
- }
2809
-
2810
2935
  class Prose extends Controller {
2811
2936
  static values = {
2812
2937
  initial: { type: Number, default: 1 },
@@ -2816,7 +2941,8 @@ class Prose extends Controller {
2816
2941
  if (!this.element.classList.contains("clamp")) {
2817
2942
  return;
2818
2943
  }
2819
- this.initialValue = parseInt(getComputedStyle(this.element).getPropertyValue("--clamp"));
2944
+ const clamp = parseInt(getComputedStyle(this.element).getPropertyValue("--clamp"), 10);
2945
+ this.initialValue = isNaN(clamp) ? 1 : clamp;
2820
2946
  this.element.addEventListener("click", () => {
2821
2947
  this.toggle();
2822
2948
  });
@@ -2836,9 +2962,19 @@ class Dialog extends Controller {
2836
2962
  static values = {
2837
2963
  resetFrame: { type: Boolean, default: false }
2838
2964
  };
2965
+ _previousFocus = null;
2839
2966
  connect() {
2840
- document.addEventListener("turbo:frame-load", this.setupDialogListeners.bind(this));
2841
- this.setupDialogListeners();
2967
+ document.addEventListener("turbo:frame-load", this.bindToggleEls);
2968
+ document.addEventListener("turbo:before-visit", this.dismiss);
2969
+ document.addEventListener("click", this.handleClickOutside);
2970
+ this.dialog.addEventListener("close", this.onClose);
2971
+ this.bindToggleEls();
2972
+ }
2973
+ disconnect() {
2974
+ document.removeEventListener("turbo:frame-load", this.bindToggleEls);
2975
+ document.removeEventListener("turbo:before-visit", this.dismiss);
2976
+ document.removeEventListener("click", this.handleClickOutside);
2977
+ this.dialog.removeEventListener("close", this.onClose);
2842
2978
  }
2843
2979
  get dialog() {
2844
2980
  return this.element;
@@ -2852,20 +2988,18 @@ class Dialog extends Controller {
2852
2988
  get frameEl() {
2853
2989
  return this.dialog.querySelector("turbo-frame");
2854
2990
  }
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));
2991
+ // Arrow function so `this` is stable for add/removeEventListener pairing.
2992
+ handleClickOutside = (event) => {
2993
+ if (event.target !== this.dialog)
2994
+ return;
2995
+ this.dismiss();
2996
+ };
2997
+ // Arrow function so `this` is stable for add/removeEventListener pairing.
2998
+ bindToggleEls = () => {
2864
2999
  this.toggleEls.forEach((el) => {
2865
3000
  el.addEventListener("click", this.toggle.bind(this));
2866
3001
  });
2867
- this.dialog.addEventListener("close", this.onClose.bind(this));
2868
- }
3002
+ };
2869
3003
  remoteContentTargetConnected() {
2870
3004
  this.show();
2871
3005
  }
@@ -2876,32 +3010,65 @@ class Dialog extends Controller {
2876
3010
  if (this.dialog.open) {
2877
3011
  this.dialog.close();
2878
3012
  } else {
3013
+ this._previousFocus = document.activeElement;
2879
3014
  this.dialog.showModal();
3015
+ this.dialog.addEventListener("keydown", this.handleFocusTrap);
2880
3016
  }
2881
3017
  }
2882
3018
  show(e = void 0) {
2883
3019
  if (e instanceof Event)
2884
3020
  e.preventDefault();
2885
3021
  this.htmlEl.classList.toggle("overflow-hidden", true);
3022
+ this._previousFocus = document.activeElement;
2886
3023
  this.dialog.showModal();
3024
+ this.dialog.addEventListener("keydown", this.handleFocusTrap);
2887
3025
  }
2888
3026
  hide({ detail }) {
2889
3027
  if (!detail.success)
2890
3028
  return;
2891
3029
  this.dismiss();
2892
3030
  }
2893
- dismiss() {
3031
+ // Arrow function so `this` is stable for add/removeEventListener pairing.
3032
+ dismiss = () => {
2894
3033
  this.dialog.close();
2895
- }
3034
+ };
2896
3035
  reloadPage() {
2897
3036
  window.location.reload();
2898
3037
  }
2899
- async onClose() {
3038
+ handleFocusTrap = (event) => {
3039
+ if (event.key !== "Tab")
3040
+ return;
3041
+ const focusable = Array.from(
3042
+ this.dialog.querySelectorAll(
3043
+ 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
3044
+ )
3045
+ ).filter((el) => !el.closest("[inert]"));
3046
+ if (focusable.length === 0)
3047
+ return;
3048
+ const first = focusable[0];
3049
+ const last = focusable[focusable.length - 1];
3050
+ if (event.shiftKey) {
3051
+ if (document.activeElement === first) {
3052
+ event.preventDefault();
3053
+ last.focus();
3054
+ }
3055
+ } else {
3056
+ if (document.activeElement === last) {
3057
+ event.preventDefault();
3058
+ first.focus();
3059
+ }
3060
+ }
3061
+ };
3062
+ // Arrow function so `this` is stable for add/removeEventListener pairing.
3063
+ onClose = async () => {
3064
+ this.dialog.removeEventListener("keydown", this.handleFocusTrap);
3065
+ this._previousFocus?.focus();
3066
+ this._previousFocus = null;
2900
3067
  this.htmlEl.classList.toggle("overflow-hidden", false);
2901
3068
  await this.delay(500);
2902
3069
  this.resetForm();
2903
3070
  this.resetFrame();
2904
- }
3071
+ };
2905
3072
  resetFrame() {
2906
3073
  if (!this.resetFrameValue || this.frameEl == null)
2907
3074
  return;
@@ -2920,18 +3087,6 @@ class Dialog extends Controller {
2920
3087
  }
2921
3088
  }
2922
3089
 
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
3090
  class Tabs extends Controller {
2936
3091
  static targets = ["tab", "panel"];
2937
3092
  connect() {
@@ -2939,6 +3094,10 @@ class Tabs extends Controller {
2939
3094
  (tab) => tab.getAttribute("aria-selected") === "true"
2940
3095
  );
2941
3096
  this.activateTab(activeIndex >= 0 ? activeIndex : 0);
3097
+ this.element.addEventListener("keydown", this.handleKeydown);
3098
+ }
3099
+ disconnect() {
3100
+ this.element.removeEventListener("keydown", this.handleKeydown);
2942
3101
  }
2943
3102
  select(event) {
2944
3103
  const tab = event.currentTarget;
@@ -2946,6 +3105,34 @@ class Tabs extends Controller {
2946
3105
  if (index >= 0)
2947
3106
  this.activateTab(index);
2948
3107
  }
3108
+ handleKeydown = (event) => {
3109
+ const tabs = this.tabTargets;
3110
+ if (tabs.length === 0)
3111
+ return;
3112
+ const currentIndex = tabs.findIndex((t) => t === document.activeElement);
3113
+ if (currentIndex < 0)
3114
+ return;
3115
+ let nextIndex;
3116
+ switch (event.key) {
3117
+ case "ArrowLeft":
3118
+ nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
3119
+ break;
3120
+ case "ArrowRight":
3121
+ nextIndex = (currentIndex + 1) % tabs.length;
3122
+ break;
3123
+ case "Home":
3124
+ nextIndex = 0;
3125
+ break;
3126
+ case "End":
3127
+ nextIndex = tabs.length - 1;
3128
+ break;
3129
+ default:
3130
+ return;
3131
+ }
3132
+ event.preventDefault();
3133
+ this.activateTab(nextIndex);
3134
+ tabs[nextIndex].focus();
3135
+ };
2949
3136
  activateTab(index) {
2950
3137
  this.tabTargets.forEach((tab, i) => {
2951
3138
  const active = i === index;
@@ -2965,12 +3152,26 @@ class Tabs extends Controller {
2965
3152
 
2966
3153
  class Tooltip extends Controller {
2967
3154
  static targets = ["popover"];
3155
+ connect() {
3156
+ document.addEventListener("keydown", this.handleKeydown);
3157
+ }
3158
+ disconnect() {
3159
+ document.removeEventListener("keydown", this.handleKeydown);
3160
+ }
2968
3161
  show() {
2969
3162
  this.popoverTarget.showPopover();
2970
3163
  }
2971
3164
  hide() {
2972
- this.popoverTarget.hidePopover();
3165
+ try {
3166
+ this.popoverTarget.hidePopover();
3167
+ } catch {
3168
+ }
2973
3169
  }
3170
+ handleKeydown = (event) => {
3171
+ if (event.key !== "Escape")
3172
+ return;
3173
+ this.hide();
3174
+ };
2974
3175
  }
2975
3176
 
2976
3177
  const NON_SUBMITTING_INPUT_TYPES = /* @__PURE__ */ new Set([
@@ -2993,16 +3194,24 @@ class Carousel extends Controller {
2993
3194
  observer = null;
2994
3195
  _currentIndex = 0;
2995
3196
  _maxUnlocked = 0;
3197
+ _lastTouchX = 0;
2996
3198
  connect() {
2997
3199
  this.setupObserver();
2998
3200
  this.updateNavigation();
2999
3201
  if (this.wizardValue) {
3000
3202
  this.element.addEventListener("keydown", this.handleKeydown);
3203
+ this.trackTarget.addEventListener("wheel", this.handleWheel, { passive: false });
3204
+ this.trackTarget.addEventListener("touchstart", this.handleTouchStart, { passive: true });
3205
+ this.trackTarget.addEventListener("touchmove", this.handleTouchMove, { passive: false });
3206
+ this.updateSlideAccessibility();
3001
3207
  }
3002
3208
  }
3003
3209
  disconnect() {
3004
3210
  this.observer?.disconnect();
3005
3211
  this.element.removeEventListener("keydown", this.handleKeydown);
3212
+ this.trackTarget.removeEventListener("wheel", this.handleWheel);
3213
+ this.trackTarget.removeEventListener("touchstart", this.handleTouchStart);
3214
+ this.trackTarget.removeEventListener("touchmove", this.handleTouchMove);
3006
3215
  }
3007
3216
  next() {
3008
3217
  this.trackTarget.scrollBy({ left: this.trackTarget.clientWidth, behavior: "smooth" });
@@ -3027,10 +3236,13 @@ class Carousel extends Controller {
3027
3236
  const nextIndex = this._currentIndex + 1;
3028
3237
  if (nextIndex > this._maxUnlocked) {
3029
3238
  this._maxUnlocked = nextIndex;
3239
+ this.updateSlideAccessibility();
3030
3240
  }
3031
3241
  this.next();
3032
3242
  }
3033
3243
  scrolled() {
3244
+ if (this.wizardValue)
3245
+ this.clampToFrontier();
3034
3246
  this.updateCurrentIndex();
3035
3247
  this.updateNavigation();
3036
3248
  }
@@ -3048,9 +3260,49 @@ class Carousel extends Controller {
3048
3260
  event.preventDefault();
3049
3261
  this.unlockNext();
3050
3262
  };
3263
+ // Blocks wheel-based horizontal scrolling past the wizard frontier.
3264
+ handleWheel = (event) => {
3265
+ const maxLeft = this._maxUnlocked * this.trackTarget.clientWidth;
3266
+ if (event.deltaX > 0 && this.trackTarget.scrollLeft >= maxLeft - 2) {
3267
+ event.preventDefault();
3268
+ }
3269
+ };
3270
+ handleTouchStart = (event) => {
3271
+ this._lastTouchX = event.touches[0].clientX;
3272
+ };
3273
+ // Blocks touch-swipe scrolling past the wizard frontier. Tracks incremental
3274
+ // delta per move event so direction is accurate mid-gesture.
3275
+ handleTouchMove = (event) => {
3276
+ const currentX = event.touches[0].clientX;
3277
+ const delta = currentX - this._lastTouchX;
3278
+ this._lastTouchX = currentX;
3279
+ const maxLeft = this._maxUnlocked * this.trackTarget.clientWidth;
3280
+ if (delta < 0 && this.trackTarget.scrollLeft >= maxLeft - 2) {
3281
+ event.preventDefault();
3282
+ }
3283
+ };
3051
3284
  get slides() {
3052
3285
  return Array.from(this.trackTarget.children);
3053
3286
  }
3287
+ // Snaps scroll position back to the frontier if the user somehow scrolled
3288
+ // past it (e.g. momentum after a partially-blocked gesture).
3289
+ clampToFrontier() {
3290
+ const maxLeft = this._maxUnlocked * this.trackTarget.clientWidth;
3291
+ if (this.trackTarget.scrollLeft > maxLeft + 2) {
3292
+ this.trackTarget.scrollLeft = maxLeft;
3293
+ }
3294
+ }
3295
+ // Applies/removes `inert` on each slide based on the wizard frontier,
3296
+ // removing locked slides from the tab order and accessibility tree.
3297
+ updateSlideAccessibility() {
3298
+ this.slides.forEach((slide, i) => {
3299
+ if (i > this._maxUnlocked) {
3300
+ slide.setAttribute("inert", "");
3301
+ } else {
3302
+ slide.removeAttribute("inert");
3303
+ }
3304
+ });
3305
+ }
3054
3306
  updateCurrentIndex() {
3055
3307
  const track = this.trackTarget;
3056
3308
  if (track.clientWidth > 0) {
@@ -3067,7 +3319,15 @@ class Carousel extends Controller {
3067
3319
  entries.forEach((entry) => {
3068
3320
  const index = this.slides.indexOf(entry.target);
3069
3321
  if (index >= 0) {
3070
- this.dotTargets[index]?.classList.toggle("active", entry.isIntersecting);
3322
+ const dot = this.dotTargets[index];
3323
+ if (!dot)
3324
+ return;
3325
+ dot.classList.toggle("active", entry.isIntersecting);
3326
+ if (entry.isIntersecting) {
3327
+ dot.setAttribute("aria-current", "true");
3328
+ } else {
3329
+ dot.removeAttribute("aria-current");
3330
+ }
3071
3331
  }
3072
3332
  });
3073
3333
  },
@@ -3082,8 +3342,9 @@ class Carousel extends Controller {
3082
3342
  if (this.hasPrevTarget)
3083
3343
  this.prevTarget.classList.toggle("hide", atStart);
3084
3344
  if (this.hasNextTarget) {
3345
+ const atLastSlide = this._currentIndex >= this.slides.length - 1;
3085
3346
  const frontierReached = this.wizardValue && this._currentIndex >= this._maxUnlocked;
3086
- this.nextTarget.classList.toggle("hide", atEnd || frontierReached);
3347
+ this.nextTarget.classList.toggle("hide", atEnd || atLastSlide || frontierReached);
3087
3348
  }
3088
3349
  if (this.wizardValue && this.hasDotTarget) {
3089
3350
  this.dotTargets.forEach((dot, i) => {
@@ -3224,23 +3485,51 @@ class FormLiveSummary extends Controller {
3224
3485
  }
3225
3486
  }
3226
3487
 
3488
+ class Form extends Controller {
3489
+ connect() {
3490
+ this.element.addEventListener("submit", this.handleSubmit);
3491
+ this.element.addEventListener("turbo:submit-end", this.handleTurboEnd);
3492
+ }
3493
+ disconnect() {
3494
+ this.element.removeEventListener("submit", this.handleSubmit);
3495
+ this.element.removeEventListener("turbo:submit-end", this.handleTurboEnd);
3496
+ }
3497
+ handleSubmit = () => {
3498
+ this.submitButtons.forEach((btn) => {
3499
+ btn.classList.add("loading");
3500
+ btn.disabled = true;
3501
+ });
3502
+ };
3503
+ // Restore buttons if the submission fails (e.g. a Turbo Frame validation
3504
+ // error that re-renders the form without a full page transition).
3505
+ handleTurboEnd = (event) => {
3506
+ if (!event.detail?.success) {
3507
+ this.submitButtons.forEach((btn) => {
3508
+ btn.classList.remove("loading");
3509
+ btn.disabled = false;
3510
+ });
3511
+ }
3512
+ };
3513
+ get submitButtons() {
3514
+ return Array.from(
3515
+ this.element.querySelectorAll('button[type="submit"], button:not([type])')
3516
+ );
3517
+ }
3518
+ }
3519
+
3227
3520
  function registerMidwestControllers(application) {
3228
3521
  application.register("midwest-card", Card);
3229
3522
  application.register("midwest-banner", Banner);
3230
- application.register("midwest-callout", Callout);
3231
3523
  application.register("midwest-input", Input);
3232
3524
  application.register("midwest-file", File);
3233
- application.register("midwest-switch", Switch);
3234
3525
  application.register("midwest-dropdown", Dropdown);
3235
- application.register("midwest-avatar", Avatar);
3236
3526
  application.register("midwest-prose", Prose);
3237
3527
  application.register("midwest-dialog", Dialog);
3238
- application.register("midwest-video", Video);
3239
- application.register("midwest-dropdown-item", DropdownItem);
3240
3528
  application.register("midwest-tabs", Tabs);
3241
3529
  application.register("midwest-tooltip", Tooltip);
3242
3530
  application.register("midwest-carousel", Carousel);
3243
3531
  application.register("midwest-form-live-summary", FormLiveSummary);
3532
+ application.register("midwest-form", Form);
3244
3533
  }
3245
3534
 
3246
3535
  export { Banner, Card, registerMidwestControllers };