kiso 0.6.2.pre → 0.6.3.pre

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e190287bffc19365594567c518032deea302c6dc22ef9a3e7aa4a5b5131c602
4
- data.tar.gz: f22a684ad94ee787f066f886ff0dd611846dd09aef7d38a89488515f1eb55f19
3
+ metadata.gz: fc54430908619f2cb1581ca13089cfceb7b9c527f7050a0079cfced84ed088ea
4
+ data.tar.gz: ea34c1970ff36648de7571bac67bec65cf5a57083a4afb4afe933c89eb27d1b2
5
5
  SHA512:
6
- metadata.gz: c892bfb57c5155239295cc7ce5cd9f3a6747504a57883b4fa49ea7b1e9f637c164285af0a8c98824972121d13c9f57f691d95f808351bacebc176255641583d0
7
- data.tar.gz: 215260ab8612f8521cc8f95357cc527968cb90be8886db63867441e7cf75714dce789b0d40fd48d624bb864bc129afb782defdaef74cb8cb554249b7b73729c5
6
+ metadata.gz: '02761138a1d470cec63228c7d3e5ffe2d9d93d271427814059cb6193450492051755bba4514f945c225a669276540b9a6bd223469ec6eb0b05b13ae361ef2601'
7
+ data.tar.gz: 2bc1f27fc05aa8da6aecf7e3b6eaae668e02afc45caa3636ebc55270738588dd4dc61102554e0cadfe33e99cba0a72a9e4e56decb3094b4646d5955a28e03ca3
@@ -0,0 +1,120 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Automatic loading state for buttons inside Turbo forms.
5
+ *
6
+ * When the button's form submits via Turbo, the controller saves the button's
7
+ * original content, injects an animated spinner SVG, and disables the button.
8
+ * On `turbo:submit-end`, the original content and state are restored.
9
+ *
10
+ * Attach via `data-controller="kiso--button-loading"` on the `<button>` element.
11
+ * The Button partial adds this automatically when `loading_auto: true`.
12
+ *
13
+ * @example
14
+ * <form action="/save" method="post" data-turbo="true">
15
+ * <button type="submit"
16
+ * data-controller="kiso--button-loading"
17
+ * data-slot="button"
18
+ * class="...">
19
+ * Save
20
+ * </button>
21
+ * </form>
22
+ *
23
+ * @fires kiso--button-loading:loading - When the button enters loading state
24
+ * @fires kiso--button-loading:complete - When the button exits loading state
25
+ */
26
+ export default class extends Controller {
27
+ connect() {
28
+ this._form = this.element.closest("form")
29
+ if (!this._form) return
30
+
31
+ this._handleSubmitStart = this._handleSubmitStart.bind(this)
32
+ this._handleSubmitEnd = this._handleSubmitEnd.bind(this)
33
+
34
+ this._form.addEventListener("turbo:submit-start", this._handleSubmitStart)
35
+ this._form.addEventListener("turbo:submit-end", this._handleSubmitEnd)
36
+ }
37
+
38
+ disconnect() {
39
+ if (!this._form) return
40
+
41
+ this._form.removeEventListener("turbo:submit-start", this._handleSubmitStart)
42
+ this._form.removeEventListener("turbo:submit-end", this._handleSubmitEnd)
43
+ this._restore()
44
+ }
45
+
46
+ /**
47
+ * Saves original button content and enters loading state.
48
+ *
49
+ * @param {CustomEvent} event - turbo:submit-start event
50
+ * @private
51
+ */
52
+ _handleSubmitStart(event) {
53
+ // Only react if this button triggered the submission
54
+ if (event.detail?.formSubmission?.submitter !== this.element) return
55
+
56
+ this._savedNodes = Array.from(this.element.childNodes).map((n) => n.cloneNode(true))
57
+ this._wasDisabled = this.element.disabled
58
+
59
+ // Create spinner SVG via DOM APIs (safe, no innerHTML)
60
+ const spinner = document.createElementNS("http://www.w3.org/2000/svg", "svg")
61
+ spinner.setAttribute("viewBox", "0 0 24 24")
62
+ spinner.setAttribute("fill", "none")
63
+ spinner.setAttribute("stroke", "currentColor")
64
+ spinner.setAttribute("stroke-width", "2")
65
+ spinner.setAttribute("stroke-linecap", "round")
66
+ spinner.setAttribute("stroke-linejoin", "round")
67
+ spinner.setAttribute("class", "animate-spin shrink-0 pointer-events-none size-4")
68
+
69
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path")
70
+ path.setAttribute("d", "M21 12a9 9 0 1 1-6.219-8.56")
71
+ spinner.appendChild(path)
72
+
73
+ // Replace content: spinner + original text
74
+ const textContent = this.element.textContent?.trim()
75
+ while (this.element.firstChild) {
76
+ this.element.removeChild(this.element.firstChild)
77
+ }
78
+ this.element.appendChild(spinner)
79
+ if (textContent) {
80
+ this.element.appendChild(document.createTextNode(` ${textContent}`))
81
+ }
82
+
83
+ this.element.disabled = true
84
+ this.element.setAttribute("aria-busy", "true")
85
+
86
+ this.dispatch("loading")
87
+ }
88
+
89
+ /**
90
+ * Restores original button content and state on turbo:submit-end.
91
+ *
92
+ * @private
93
+ */
94
+ _handleSubmitEnd() {
95
+ this._restore()
96
+ this.dispatch("complete")
97
+ }
98
+
99
+ /**
100
+ * Restores the button to its pre-loading state.
101
+ *
102
+ * @private
103
+ */
104
+ _restore() {
105
+ if (!this._savedNodes) return
106
+
107
+ while (this.element.firstChild) {
108
+ this.element.removeChild(this.element.firstChild)
109
+ }
110
+ for (const node of this._savedNodes) {
111
+ this.element.appendChild(node)
112
+ }
113
+
114
+ this.element.disabled = this._wasDisabled
115
+ this.element.removeAttribute("aria-busy")
116
+
117
+ this._savedNodes = undefined
118
+ this._wasDisabled = undefined
119
+ }
120
+ }
@@ -158,7 +158,10 @@ export default class extends Controller {
158
158
  if (item.dataset.disabled === "true") return
159
159
 
160
160
  this.dispatch("select", { detail: { item } })
161
- this.close()
161
+ // Defer close to the next frame so other click handlers on the item
162
+ // (e.g. dialog triggers, custom actions) complete before the menu
163
+ // hides and removes elements from view. See #233.
164
+ requestAnimationFrame(() => this.close())
162
165
  }
163
166
 
164
167
  /**
@@ -6,6 +6,7 @@ declare const KisoUi: {
6
6
 
7
7
  export default KisoUi
8
8
  export const KisoAlertController: typeof Controller
9
+ export const KisoButtonLoadingController: typeof Controller
9
10
  export const KisoComboboxController: typeof Controller
10
11
  export const KisoCommandController: typeof Controller
11
12
  export const KisoCommandDialogController: typeof Controller
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import KisoAlertController from "./alert_controller.js"
22
+ import KisoButtonLoadingController from "./button_loading_controller.js"
22
23
  import KisoComboboxController from "./combobox_controller.js"
23
24
  import KisoCommandController from "./command_controller.js"
24
25
  import KisoCommandDialogController from "./command_dialog_controller.js"
@@ -43,6 +44,7 @@ const KisoUi = {
43
44
  */
44
45
  start(application) {
45
46
  application.register("kiso--alert", KisoAlertController)
47
+ application.register("kiso--button-loading", KisoButtonLoadingController)
46
48
  application.register("kiso--combobox", KisoComboboxController)
47
49
  application.register("kiso--command", KisoCommandController)
48
50
  application.register("kiso--command-dialog", KisoCommandDialogController)
@@ -64,6 +66,7 @@ const KisoUi = {
64
66
  export default KisoUi
65
67
  export {
66
68
  KisoAlertController,
69
+ KisoButtonLoadingController,
67
70
  KisoComboboxController,
68
71
  KisoCommandController,
69
72
  KisoCommandDialogController,
@@ -1,14 +1,17 @@
1
1
  <%# locals: (color: :primary, variant: :solid, size: :md, block: false,
2
2
  type: :button, href: nil, method: nil, disabled: false,
3
- loading: false, form: {}, css_classes: "", **component_options) %>
3
+ loading: false, loading_auto: false, form: {}, css_classes: "", **component_options) %>
4
4
  <%# Polymorphic button that renders as <button>, <a>, or button_to depending on props.
5
5
  With href: renders an anchor tag. With href: + method: (non-GET) renders a Rails
6
6
  button_to form for safe non-GET navigation. Without href: renders a plain <button>.
7
- With loading: true, prepends an animated spinner and disables the button. %>
7
+ With loading: true, prepends an animated spinner and disables the button.
8
+ With loading_auto: true, attaches kiso--button-loading Stimulus controller for
9
+ automatic loading state on Turbo form submissions. %>
8
10
  <%
9
11
  css = Kiso::Themes::Button.render(
10
12
  color: color, variant: variant, size: size, block: block, class: css_classes)
11
- data = kiso_prepare_options(component_options, slot: "button")
13
+ data = kiso_prepare_options(component_options, slot: "button",
14
+ **(loading_auto ? { controller: "kiso--button-loading" } : {}))
12
15
  use_button_to = href.present? && method.present? && method.to_s != "get"
13
16
  is_disabled = disabled || loading
14
17
  component_options[:"aria-busy"] = true if loading
data/lib/kiso/version.rb CHANGED
@@ -5,5 +5,5 @@ module Kiso
5
5
  # Updated by +bin/release+.
6
6
  #
7
7
  # @return [String]
8
- VERSION = "0.6.2.pre"
8
+ VERSION = "0.6.3.pre"
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kiso
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2.pre
4
+ version: 0.6.3.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Clarke
@@ -100,6 +100,7 @@ files:
100
100
  - app/helpers/kiso/theme_helper.rb
101
101
  - app/helpers/kiso/ui_context_helper.rb
102
102
  - app/javascript/controllers/kiso/alert_controller.js
103
+ - app/javascript/controllers/kiso/button_loading_controller.js
103
104
  - app/javascript/controllers/kiso/combobox_controller.js
104
105
  - app/javascript/controllers/kiso/command_controller.js
105
106
  - app/javascript/controllers/kiso/command_dialog_controller.js