kiso 0.6.1.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 +4 -4
- data/app/assets/tailwind/kiso/button.css +8 -9
- data/app/javascript/controllers/kiso/button_loading_controller.js +120 -0
- data/app/javascript/controllers/kiso/dropdown_menu_controller.js +4 -1
- data/app/javascript/controllers/kiso/index.d.ts +1 -0
- data/app/javascript/controllers/kiso/index.js +3 -0
- data/app/views/kiso/components/_button.html.erb +15 -6
- data/app/views/kiso/components/dropdown_menu/_item.html.erb +39 -14
- data/lib/kiso/themes/button.rb +1 -1
- data/lib/kiso/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fc54430908619f2cb1581ca13089cfceb7b9c527f7050a0079cfced84ed088ea
|
|
4
|
+
data.tar.gz: ea34c1970ff36648de7571bac67bec65cf5a57083a4afb4afe933c89eb27d1b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '02761138a1d470cec63228c7d3e5ffe2d9d93d271427814059cb6193450492051755bba4514f945c225a669276540b9a6bd223469ec6eb0b05b13ae361ef2601'
|
|
7
|
+
data.tar.gz: 2bc1f27fc05aa8da6aecf7e3b6eaae668e02afc45caa3636ebc55270738588dd4dc61102554e0cadfe33e99cba0a72a9e4e56decb3094b4646d5955a28e03ca3
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/* ── Button ──────────────────────────────────────────────────────────────────
|
|
2
2
|
Default display value for the Button component.
|
|
3
3
|
|
|
4
|
-
Why CSS
|
|
5
|
-
The theme module
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
override it naturally, while still restoring inline-flex when `hidden` is
|
|
11
|
-
removed.
|
|
4
|
+
Why CSS in addition to the theme class?
|
|
5
|
+
The theme module includes `inline-flex` so the class string is
|
|
6
|
+
self-contained (works on link_to, button_to, etc. without data-slot).
|
|
7
|
+
This @layer components rule provides a lower-priority fallback for
|
|
8
|
+
kui(:button) elements so that utility classes like `hidden` can override
|
|
9
|
+
display naturally, restoring inline-flex when `hidden` is removed.
|
|
12
10
|
|
|
13
|
-
Fixes: https://github.com/steveclarke/kiso/issues/200
|
|
11
|
+
Fixes: https://github.com/steveclarke/kiso/issues/200
|
|
12
|
+
See also: https://github.com/steveclarke/kiso/issues/229 */
|
|
14
13
|
|
|
15
14
|
@layer components {
|
|
16
15
|
[data-slot="button"] {
|
|
@@ -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
|
-
|
|
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,20 @@
|
|
|
1
1
|
<%# locals: (color: :primary, variant: :solid, size: :md, block: false,
|
|
2
2
|
type: :button, href: nil, method: nil, disabled: false,
|
|
3
|
-
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
|
-
button_to form for safe non-GET navigation. Without href: renders a plain <button>.
|
|
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.
|
|
8
|
+
With loading_auto: true, attaches kiso--button-loading Stimulus controller for
|
|
9
|
+
automatic loading state on Turbo form submissions. %>
|
|
7
10
|
<%
|
|
8
11
|
css = Kiso::Themes::Button.render(
|
|
9
12
|
color: color, variant: variant, size: size, block: block, class: css_classes)
|
|
10
|
-
data = kiso_prepare_options(component_options, slot: "button"
|
|
13
|
+
data = kiso_prepare_options(component_options, slot: "button",
|
|
14
|
+
**(loading_auto ? { controller: "kiso--button-loading" } : {}))
|
|
11
15
|
use_button_to = href.present? && method.present? && method.to_s != "get"
|
|
16
|
+
is_disabled = disabled || loading
|
|
17
|
+
component_options[:"aria-busy"] = true if loading
|
|
12
18
|
%>
|
|
13
19
|
<% if use_button_to %>
|
|
14
20
|
<%= button_to href,
|
|
@@ -16,21 +22,24 @@
|
|
|
16
22
|
class: css,
|
|
17
23
|
form_class: "contents",
|
|
18
24
|
data: data,
|
|
19
|
-
disabled:
|
|
25
|
+
disabled: is_disabled || nil,
|
|
20
26
|
form: form.presence,
|
|
21
27
|
**component_options do %>
|
|
28
|
+
<%= kiso_component_icon(:spinner, class: "animate-spin") if loading %>
|
|
22
29
|
<%= yield %>
|
|
23
30
|
<% end %>
|
|
24
31
|
<% elsif href.present? %>
|
|
25
32
|
<% component_options[:href] = href
|
|
26
|
-
component_options[:"aria-disabled"] = true if
|
|
33
|
+
component_options[:"aria-disabled"] = true if is_disabled %>
|
|
27
34
|
<%= content_tag :a, class: css, data: data, **component_options do %>
|
|
35
|
+
<%= kiso_component_icon(:spinner, class: "animate-spin") if loading %>
|
|
28
36
|
<%= yield %>
|
|
29
37
|
<% end %>
|
|
30
38
|
<% else %>
|
|
31
39
|
<% component_options[:type] = type
|
|
32
|
-
component_options[:disabled] = true if
|
|
40
|
+
component_options[:disabled] = true if is_disabled %>
|
|
33
41
|
<%= content_tag :button, class: css, data: data, **component_options do %>
|
|
42
|
+
<%= kiso_component_icon(:spinner, class: "animate-spin") if loading %>
|
|
34
43
|
<%= yield %>
|
|
35
44
|
<% end %>
|
|
36
45
|
<% end %>
|
|
@@ -1,15 +1,40 @@
|
|
|
1
|
-
<%# locals: (variant: :default, inset: false, disabled: false,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
<%# locals: (variant: :default, inset: false, disabled: false,
|
|
2
|
+
href: nil, method: nil, form: {},
|
|
3
|
+
css_classes: "", **component_options) %>
|
|
4
|
+
<%# Clickable menu action. Polymorphic tag: <div> by default, <a> when href:
|
|
5
|
+
is provided, button_to form when href: + method: (non-GET). %>
|
|
6
|
+
<%
|
|
7
|
+
css = Kiso::Themes::DropdownMenuItem.render(variant: variant, class: css_classes)
|
|
8
|
+
data = kiso_prepare_options(component_options, slot: "dropdown-menu-item",
|
|
9
|
+
kiso__dropdown_menu_target: "item",
|
|
10
|
+
action: "click->kiso--dropdown-menu#selectItem",
|
|
11
|
+
inset: (inset ? "" : nil),
|
|
12
|
+
variant: (variant == :destructive ? "destructive" : nil),
|
|
13
|
+
disabled: (disabled ? "true" : nil))
|
|
14
|
+
component_options[:role] = "menuitem"
|
|
15
|
+
component_options[:tabindex] = "-1"
|
|
16
|
+
component_options[:aria] = { disabled: (disabled || nil) }
|
|
17
|
+
use_button_to = href.present? && method.present? && method.to_s != "get"
|
|
18
|
+
%>
|
|
19
|
+
<% if use_button_to %>
|
|
20
|
+
<%= button_to href,
|
|
21
|
+
method: method,
|
|
22
|
+
class: css,
|
|
23
|
+
form_class: "contents w-full",
|
|
24
|
+
data: data,
|
|
25
|
+
disabled: disabled || nil,
|
|
26
|
+
form: form.presence,
|
|
27
|
+
**component_options do %>
|
|
28
|
+
<%= yield %>
|
|
29
|
+
<% end %>
|
|
30
|
+
<% elsif href.present? %>
|
|
31
|
+
<% component_options[:href] = href
|
|
32
|
+
component_options[:"aria-disabled"] = true if disabled %>
|
|
33
|
+
<%= content_tag :a, class: css, data: data, **component_options do %>
|
|
34
|
+
<%= yield %>
|
|
35
|
+
<% end %>
|
|
36
|
+
<% else %>
|
|
37
|
+
<%= content_tag :div, class: css, data: data, **component_options do %>
|
|
38
|
+
<%= yield %>
|
|
39
|
+
<% end %>
|
|
15
40
|
<% end %>
|
data/lib/kiso/themes/button.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Kiso
|
|
|
14
14
|
# - +size+ — :xs, :sm, :md (default), :lg, :xl
|
|
15
15
|
# - +block+ — +true+ for full-width, +false+ (default)
|
|
16
16
|
Button = ClassVariants.build(
|
|
17
|
-
base: "items-center justify-center gap-2 font-medium whitespace-nowrap shrink-0 " \
|
|
17
|
+
base: "inline-flex items-center justify-center gap-2 font-medium whitespace-nowrap shrink-0 " \
|
|
18
18
|
"transition-all " \
|
|
19
19
|
"focus-visible:outline-2 focus-visible:outline-offset-2 " \
|
|
20
20
|
"disabled:pointer-events-none disabled:opacity-50 " \
|
data/lib/kiso/version.rb
CHANGED
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.
|
|
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
|