modal_stack 0.3.0 → 0.4.1
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/CHANGELOG.md +33 -0
- data/README.md +113 -32
- data/app/assets/javascripts/modal_stack.js +488 -50
- data/app/assets/stylesheets/modal_stack/bootstrap.css +113 -3
- data/app/assets/stylesheets/modal_stack/tailwind_v3.css +63 -2
- data/app/assets/stylesheets/modal_stack/tailwind_v4.css +63 -2
- data/app/assets/stylesheets/modal_stack/vanilla.css +121 -3
- data/app/javascript/modal_stack/controllers/modal_stack_back_link_controller.js +32 -0
- data/app/javascript/modal_stack/controllers/modal_stack_controller.js +50 -0
- data/app/javascript/modal_stack/install.js +7 -1
- data/app/javascript/modal_stack/orchestrator.js +53 -7
- data/app/javascript/modal_stack/orchestrator.test.js +96 -0
- data/app/javascript/modal_stack/runtime.js +167 -5
- data/app/javascript/modal_stack/runtime.test.js +83 -0
- data/app/javascript/modal_stack/state.js +319 -34
- data/app/javascript/modal_stack/state.test.js +394 -9
- data/app/views/modal_stack/_dialog.html.erb +1 -0
- data/app/views/modal_stack/_panel.html.erb +4 -0
- data/lib/generators/modal_stack/install/templates/initializer.rb +9 -0
- data/lib/generators/modal_stack/views/templates/_dialog.html.erb +9 -0
- data/lib/generators/modal_stack/views/templates/_panel.html.erb +18 -0
- data/lib/generators/modal_stack/views/views_generator.rb +50 -0
- data/lib/modal_stack/capybara.rb +21 -0
- data/lib/modal_stack/configuration.rb +37 -16
- data/lib/modal_stack/engine.rb +2 -0
- data/lib/modal_stack/helpers/modal_back_link_helper.rb +48 -0
- data/lib/modal_stack/helpers/modal_stack_assets_helper.rb +1 -1
- data/lib/modal_stack/helpers/modal_stack_container_helper.rb +42 -15
- data/lib/modal_stack/turbo_streams_extension.rb +56 -0
- data/lib/modal_stack/version.rb +1 -1
- data/lib/modal_stack.rb +5 -1
- metadata +9 -2
|
@@ -20,6 +20,7 @@ module ModalStack
|
|
|
20
20
|
VARIANTS = %i[modal drawer bottom_sheet confirmation].freeze
|
|
21
21
|
SIZES = %i[sm md lg xl].freeze
|
|
22
22
|
MAX_DEPTH_STRATEGIES = %i[raise warn silent].freeze
|
|
23
|
+
PATH_TRANSITIONS = %i[slide fade none].freeze
|
|
23
24
|
|
|
24
25
|
attr_accessor :default_classes,
|
|
25
26
|
:request_header,
|
|
@@ -37,24 +38,12 @@ module ModalStack
|
|
|
37
38
|
:default_size,
|
|
38
39
|
:default_dismissible,
|
|
39
40
|
:max_depth,
|
|
40
|
-
:max_depth_strategy
|
|
41
|
+
:max_depth_strategy,
|
|
42
|
+
:default_path_transition
|
|
41
43
|
|
|
42
44
|
def initialize
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@default_variant = :modal
|
|
46
|
-
@default_size = :md
|
|
47
|
-
@default_dismissible = true
|
|
48
|
-
@max_depth = 5
|
|
49
|
-
@max_depth_strategy = :warn
|
|
50
|
-
@request_header = "X-Modal-Stack-Request"
|
|
51
|
-
@dialog_id = "modal-stack-root"
|
|
52
|
-
@stack_root_data_attribute = "modal-stack"
|
|
53
|
-
@respect_reduced_motion = true
|
|
54
|
-
@replace_turbo_confirm = false
|
|
55
|
-
@i18n_scope = "modal_stack"
|
|
56
|
-
@initializer_version = nil
|
|
57
|
-
@silence_initializer_warning = false
|
|
45
|
+
apply_behavior_defaults
|
|
46
|
+
apply_naming_defaults
|
|
58
47
|
@default_classes = default_classes_hash
|
|
59
48
|
end
|
|
60
49
|
|
|
@@ -115,8 +104,40 @@ module ModalStack
|
|
|
115
104
|
@max_depth_strategy = value
|
|
116
105
|
end
|
|
117
106
|
|
|
107
|
+
def default_path_transition=(value)
|
|
108
|
+
value = value.to_sym
|
|
109
|
+
unless PATH_TRANSITIONS.include?(value)
|
|
110
|
+
raise ArgumentError,
|
|
111
|
+
"default_path_transition must be one of #{PATH_TRANSITIONS.inspect}, got #{value.inspect}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
@default_path_transition = value
|
|
115
|
+
end
|
|
116
|
+
|
|
118
117
|
private
|
|
119
118
|
|
|
119
|
+
def apply_behavior_defaults
|
|
120
|
+
@css_provider = :tailwind_v3
|
|
121
|
+
@assets_mode = :auto
|
|
122
|
+
@default_variant = :modal
|
|
123
|
+
@default_size = :md
|
|
124
|
+
@default_dismissible = true
|
|
125
|
+
@max_depth = 5
|
|
126
|
+
@max_depth_strategy = :warn
|
|
127
|
+
@default_path_transition = :slide
|
|
128
|
+
@respect_reduced_motion = true
|
|
129
|
+
@replace_turbo_confirm = false
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def apply_naming_defaults
|
|
133
|
+
@request_header = "X-Modal-Stack-Request"
|
|
134
|
+
@dialog_id = "modal-stack-root"
|
|
135
|
+
@stack_root_data_attribute = "modal-stack"
|
|
136
|
+
@i18n_scope = "modal_stack"
|
|
137
|
+
@initializer_version = nil
|
|
138
|
+
@silence_initializer_warning = false
|
|
139
|
+
end
|
|
140
|
+
|
|
120
141
|
def default_classes_hash
|
|
121
142
|
{
|
|
122
143
|
modal_panel: nil,
|
data/lib/modal_stack/engine.rb
CHANGED
|
@@ -9,9 +9,11 @@ module ModalStack
|
|
|
9
9
|
require "modal_stack/helpers/modal_link_helper"
|
|
10
10
|
require "modal_stack/helpers/modal_stack_container_helper"
|
|
11
11
|
require "modal_stack/helpers/modal_stack_assets_helper"
|
|
12
|
+
require "modal_stack/helpers/modal_back_link_helper"
|
|
12
13
|
include ModalStack::Helpers::ModalLinkHelper
|
|
13
14
|
include ModalStack::Helpers::ModalStackContainerHelper
|
|
14
15
|
include ModalStack::Helpers::ModalStackAssetsHelper
|
|
16
|
+
include ModalStack::Helpers::ModalBackLinkHelper
|
|
15
17
|
end
|
|
16
18
|
end
|
|
17
19
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ModalStack
|
|
4
|
+
module Helpers
|
|
5
|
+
# Renders a button (or link) that steps back through the top layer's
|
|
6
|
+
# path. Wired to the `modal-stack-back-link` Stimulus controller, which
|
|
7
|
+
# delegates to `orchestrator.pathBack`.
|
|
8
|
+
#
|
|
9
|
+
# <%= modal_back_link "Back" %>
|
|
10
|
+
# <%= modal_back_link "Back to step 1", steps: 2 %>
|
|
11
|
+
# <%= modal_back_link class: "btn btn-link" do %>
|
|
12
|
+
# <span aria-hidden="true">←</span> Back
|
|
13
|
+
# <% end %>
|
|
14
|
+
module ModalBackLinkHelper
|
|
15
|
+
LINK_CONTROLLER = "modal-stack-back-link"
|
|
16
|
+
LINK_CLICK_ACTION = "click->modal-stack-back-link#trigger"
|
|
17
|
+
|
|
18
|
+
def modal_back_link(name = nil, options = {}, &block) # rubocop:disable Metrics/CyclomaticComplexity
|
|
19
|
+
if block
|
|
20
|
+
options = name.is_a?(Hash) ? name : {}
|
|
21
|
+
name = capture(&block)
|
|
22
|
+
end
|
|
23
|
+
options = options.dup
|
|
24
|
+
steps = Integer(options.delete(:steps) || 1)
|
|
25
|
+
raise ArgumentError, "steps must be a positive integer, got #{steps.inspect}" if steps < 1
|
|
26
|
+
|
|
27
|
+
options[:data] = back_link_data(options.delete(:data) || {}, steps)
|
|
28
|
+
options[:type] ||= "button"
|
|
29
|
+
|
|
30
|
+
button_tag(name || I18n.t("modal_stack.back", default: "Back"), **options)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def back_link_data(existing, steps)
|
|
36
|
+
existing.merge(
|
|
37
|
+
controller: merged_token(existing[:controller], LINK_CONTROLLER),
|
|
38
|
+
action: merged_token(existing[:action], LINK_CLICK_ACTION),
|
|
39
|
+
modal_stack_back_link_steps_value: steps
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def merged_token(existing, addition)
|
|
44
|
+
[existing, addition].compact.join(" ").strip
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -28,7 +28,7 @@ module ModalStack
|
|
|
28
28
|
attrs[:id] ||= config.dialog_id
|
|
29
29
|
attrs[:data] = build_dialog_data(attrs[:data], config)
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
render partial: "modal_stack/dialog", locals: { dialog_attrs: attrs }
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# Merges caller-provided data attrs with the gem-managed ones (controller,
|
|
@@ -12,24 +12,51 @@ module ModalStack
|
|
|
12
12
|
module ModalStackContainerHelper
|
|
13
13
|
DEFAULT_SIZE = :md
|
|
14
14
|
|
|
15
|
-
def modal_stack_container(size: DEFAULT_SIZE, dismissible: true, variant: :modal, side: nil, width: nil, height: nil,
|
|
16
|
-
&)
|
|
15
|
+
def modal_stack_container(size: DEFAULT_SIZE, dismissible: true, variant: :modal, side: nil, width: nil, height: nil,
|
|
16
|
+
back: false, transition: nil, html: {}, &)
|
|
17
|
+
attrs = build_panel_attrs(size: size, variant: variant, side: side,
|
|
18
|
+
dismissible: dismissible, width: width,
|
|
19
|
+
height: height, transition: transition, html: html)
|
|
20
|
+
body = capture(&)
|
|
21
|
+
back_html = back ? modal_stack_container_back_button : nil
|
|
22
|
+
|
|
23
|
+
render partial: "modal_stack/panel", locals: {
|
|
24
|
+
content: body, back_button: back_html, wrapper_attrs: attrs,
|
|
25
|
+
size: size, variant: variant, dismissible: dismissible,
|
|
26
|
+
side: side, width: width, height: height, transition: transition
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def build_panel_attrs(size:, variant:, side:, dismissible:, width:, height:, transition:, html:)
|
|
17
33
|
classes = ["modal-stack__panel", "modal-stack__panel--#{variant}", "modal-stack__panel--size-#{size}"]
|
|
18
34
|
classes << "modal-stack__panel--side-#{side}" if side
|
|
19
35
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
data = {
|
|
37
|
+
modal_stack_size: size, modal_stack_variant: variant,
|
|
38
|
+
modal_stack_dismissible: dismissible.to_s, modal_stack_side: side,
|
|
39
|
+
modal_stack_width: width, modal_stack_height: height,
|
|
40
|
+
modal_stack_transition: transition
|
|
41
|
+
}.merge(html.fetch(:data, {})).compact
|
|
42
|
+
|
|
43
|
+
{ class: [classes, html[:class]].compact.join(" "), data: data }
|
|
44
|
+
.merge(html.except(:class, :data))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The back button stays in the DOM at all depths and is hidden by CSS
|
|
48
|
+
# when the layer is on its first frame (`[data-frame-depth="1"]`).
|
|
49
|
+
# That keeps the wizard structure stable across path_to/path_back —
|
|
50
|
+
# no Stimulus state needed.
|
|
51
|
+
def modal_stack_container_back_button
|
|
52
|
+
label = (defined?(I18n) ? I18n.t("modal_stack.back", default: "Back") : "Back")
|
|
53
|
+
button_tag(
|
|
54
|
+
label,
|
|
55
|
+
type: "button",
|
|
56
|
+
class: "modal-stack__panel-back",
|
|
57
|
+
data: { action: "click->modal-stack#pathBack" },
|
|
58
|
+
aria: { label: label }
|
|
59
|
+
)
|
|
33
60
|
end
|
|
34
61
|
end
|
|
35
62
|
end
|
|
@@ -6,6 +6,7 @@ module ModalStack
|
|
|
6
6
|
# so the standard `turbo_stream.foo(...)` form keeps working alongside.
|
|
7
7
|
module TurboStreamsExtension
|
|
8
8
|
HISTORY_MODES = %i[push replace].freeze
|
|
9
|
+
PATH_TRANSITIONS = %i[slide fade none].freeze
|
|
9
10
|
|
|
10
11
|
# Push a new layer on top of the stack. The content is rendered using
|
|
11
12
|
# the same options as Turbo's standard stream actions
|
|
@@ -64,10 +65,65 @@ module ModalStack
|
|
|
64
65
|
turbo_stream_action_tag(:modal_close_all, target: ModalStack::TARGET_ID)
|
|
65
66
|
end
|
|
66
67
|
|
|
68
|
+
# Navigate forward inside the top layer's path. The current frame is
|
|
69
|
+
# cached in memory so a subsequent `modal_path_back` (or browser back)
|
|
70
|
+
# restores it without a network round-trip. Pass `stale: true` (or set
|
|
71
|
+
# `X-Modal-Stack-Stale: true` on the response) to force a refetch on
|
|
72
|
+
# back.
|
|
73
|
+
def modal_path_to(content = nil, url: nil, transition: nil, stale: false, layer_id: nil, **rendering, &)
|
|
74
|
+
template = render_template(ModalStack::TARGET_ID, content, **rendering, &)
|
|
75
|
+
turbo_stream_action_tag(
|
|
76
|
+
:modal_path_to,
|
|
77
|
+
target: ModalStack::TARGET_ID,
|
|
78
|
+
template: template,
|
|
79
|
+
data: modal_data(
|
|
80
|
+
url: url,
|
|
81
|
+
transition: validate_path_transition(resolved_path_transition(transition)),
|
|
82
|
+
stale: stale ? "true" : nil,
|
|
83
|
+
layer_id: layer_id
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Step back through the top layer's path. Defaults to one frame; pass
|
|
89
|
+
# `steps: N` to collapse multiple frames at once. Clamps at the first
|
|
90
|
+
# frame — does not close the layer.
|
|
91
|
+
def modal_path_back(steps: 1, transition: nil)
|
|
92
|
+
n = Integer(steps)
|
|
93
|
+
raise ArgumentError, "steps must be a positive integer, got #{steps.inspect}" if n < 1
|
|
94
|
+
|
|
95
|
+
turbo_stream_action_tag(
|
|
96
|
+
:modal_path_back,
|
|
97
|
+
target: ModalStack::TARGET_ID,
|
|
98
|
+
data: modal_data(
|
|
99
|
+
steps: n,
|
|
100
|
+
transition: validate_path_transition(resolved_path_transition(transition))
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
67
105
|
private
|
|
68
106
|
|
|
69
107
|
def modal_data(**attrs)
|
|
70
108
|
attrs.compact
|
|
71
109
|
end
|
|
110
|
+
|
|
111
|
+
def validate_path_transition(value)
|
|
112
|
+
return nil if value.nil?
|
|
113
|
+
|
|
114
|
+
sym = value.to_sym
|
|
115
|
+
unless PATH_TRANSITIONS.include?(sym)
|
|
116
|
+
raise ArgumentError, "transition must be one of #{PATH_TRANSITIONS.inspect}, got #{value.inspect}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
sym
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Falls back to the configured default when a call site doesn't
|
|
123
|
+
# specify a transition. Pass `transition: :none` to disable
|
|
124
|
+
# explicitly.
|
|
125
|
+
def resolved_path_transition(value)
|
|
126
|
+
value || ModalStack.configuration.default_path_transition
|
|
127
|
+
end
|
|
72
128
|
end
|
|
73
129
|
end
|
data/lib/modal_stack/version.rb
CHANGED
data/lib/modal_stack.rb
CHANGED
|
@@ -10,11 +10,15 @@ module ModalStack
|
|
|
10
10
|
# at the application level (config.dialog_id =, config.request_header =).
|
|
11
11
|
TARGET_ID = "modal-stack-root"
|
|
12
12
|
REQUEST_HEADER = "X-Modal-Stack-Request"
|
|
13
|
+
# Response header set by `path_to` controllers to mark the just-rendered
|
|
14
|
+
# frame as stale — the JS runtime refetches instead of restoring from its
|
|
15
|
+
# in-memory cache when the user steps back to it.
|
|
16
|
+
STALE_HEADER = "X-Modal-Stack-Stale"
|
|
13
17
|
|
|
14
18
|
# Bumped when config/initializers/modal_stack.rb gains/loses an option,
|
|
15
19
|
# so apps that haven't regenerated their initializer get a one-line
|
|
16
20
|
# boot warning. Independent from the gem's VERSION.
|
|
17
|
-
INITIALIZER_VERSION = "0.
|
|
21
|
+
INITIALIZER_VERSION = "0.4.0"
|
|
18
22
|
|
|
19
23
|
class << self
|
|
20
24
|
def configuration
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: modal_stack
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Florian Gagnaire
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|
|
@@ -73,6 +73,7 @@ files:
|
|
|
73
73
|
- app/assets/stylesheets/modal_stack/tailwind_v3.css
|
|
74
74
|
- app/assets/stylesheets/modal_stack/tailwind_v4.css
|
|
75
75
|
- app/assets/stylesheets/modal_stack/vanilla.css
|
|
76
|
+
- app/javascript/modal_stack/controllers/modal_stack_back_link_controller.js
|
|
76
77
|
- app/javascript/modal_stack/controllers/modal_stack_controller.js
|
|
77
78
|
- app/javascript/modal_stack/controllers/modal_stack_link_controller.js
|
|
78
79
|
- app/javascript/modal_stack/index.js
|
|
@@ -84,8 +85,13 @@ files:
|
|
|
84
85
|
- app/javascript/modal_stack/state.js
|
|
85
86
|
- app/javascript/modal_stack/state.test.js
|
|
86
87
|
- app/views/layouts/modal.html.erb
|
|
88
|
+
- app/views/modal_stack/_dialog.html.erb
|
|
89
|
+
- app/views/modal_stack/_panel.html.erb
|
|
87
90
|
- lib/generators/modal_stack/install/install_generator.rb
|
|
88
91
|
- lib/generators/modal_stack/install/templates/initializer.rb
|
|
92
|
+
- lib/generators/modal_stack/views/templates/_dialog.html.erb
|
|
93
|
+
- lib/generators/modal_stack/views/templates/_panel.html.erb
|
|
94
|
+
- lib/generators/modal_stack/views/views_generator.rb
|
|
89
95
|
- lib/modal_stack.rb
|
|
90
96
|
- lib/modal_stack/capybara.rb
|
|
91
97
|
- lib/modal_stack/capybara/minitest.rb
|
|
@@ -93,6 +99,7 @@ files:
|
|
|
93
99
|
- lib/modal_stack/configuration.rb
|
|
94
100
|
- lib/modal_stack/controller_extensions.rb
|
|
95
101
|
- lib/modal_stack/engine.rb
|
|
102
|
+
- lib/modal_stack/helpers/modal_back_link_helper.rb
|
|
96
103
|
- lib/modal_stack/helpers/modal_link_helper.rb
|
|
97
104
|
- lib/modal_stack/helpers/modal_stack_assets_helper.rb
|
|
98
105
|
- lib/modal_stack/helpers/modal_stack_container_helper.rb
|