ultimate_turbo_modal 3.2.1 → 3.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f0c59204e8c87cee87fc85083ef4147930a7a493b5d9c6a8ddf7e1ac76f04359
4
- data.tar.gz: 92c975e4676ec8ae2ae953be97f6beea70d2aa3d90b6403564af08950710a058
3
+ metadata.gz: e8449cf081c6d1dfc2b84106cc16b316fb6a15b25031a2a9dc94c2ce4bf99b35
4
+ data.tar.gz: b1067b8a6ef21a15e9556bee1e8a0ceb45fdb453ff8fa772eda0ad5191b7d669
5
5
  SHA512:
6
- metadata.gz: 918fac0dfe7519413443eac794e82b31d8014ecdbaae3279baeb0474fa00f2445b59c57f249ff35f7e5bcc0ab6c3dd6f91fecdaf3e30e34c78326b2d02e25082
7
- data.tar.gz: 6cf8a3e810da824c8bb2cf6eb1f5c4fc9ef75671dbae67486289a927599c2c3984fa5d046fa2ac17562e78db63635145016bb4a0f1d46441b26e44c7b2d44b53
6
+ metadata.gz: 86b485906013944b475e4f0aece1ee980dd226aa160b8daed3628fb84b8b7cbbea8b015086310372f2fceab89d34372a18acd0b4cd76720dbf5af0cd18574f74
7
+ data.tar.gz: 5e6753fa5decf7401e07a2bda70dcef18a25fe56c2c487f0eb76e66dc010b556b52a2d7893201de73815431bd95600a80861de94e7758d863d3e98949d67783f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [3.4.0] - 2026-09-10
4
+
5
+ - Added optional Turbo Confirm support: `data-turbo-confirm` prompts can now render as a UTMR dialog instead of the browser's `window.confirm`.
6
+ - Fixed `<%= m.title do %>` and `<%= m.footer do %>` printing a `Proc` into the modal body. Both forms now work, with or without the `=`.
7
+
8
+ ## [3.3.0] - 2026-09-05
9
+
10
+ - Added `close_on_submit`, which keeps a modal or drawer open after a successful form submission. Individual forms can override it with `data-modal-close-on-submit`.
11
+ - Drawers now support the `advance` option to push their URL to browser history, matching modals. Defaults to `false`.
12
+ - Fixed modals disappearing during in-frame updates that morph an already-open dialog.
13
+ - Fixed stale close cleanup interfering with a newer modal opened in the same frame.
14
+ - Fixed the vanilla flavor loading no styles on apps without a JavaScript bundler. The stylesheet now ships with the gem and can be included with `stylesheet_link_tag "ultimate_turbo_modal"`.
15
+
3
16
  ## [3.2.1] - 2026-05-07
4
17
 
5
18
  - Fixed drawers closing abruptly when pressing Escape after dismissing a modal opened from inside the drawer.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ultimate_turbo_modal (3.2.1)
4
+ ultimate_turbo_modal (3.4.0)
5
5
  actionpack (>= 8.0)
6
6
  activesupport (>= 8.0)
7
7
  phlex-rails (>= 2.0)
data/README.md CHANGED
@@ -26,6 +26,30 @@ $ bundle add ultimate_turbo_modal
26
26
  $ bundle exec rails g ultimate_turbo_modal:install
27
27
  ```
28
28
 
29
+ ### Stylesheet
30
+
31
+ The `tailwind` and `custom` flavors are styled entirely by the classes in their flavor
32
+ file, so they need no stylesheet.
33
+
34
+ The `vanilla` flavor ships its styles as a CSS file. The install generator adds it to
35
+ `app/views/layouts/application.html.erb` for you; if you need to add it by hand, put this
36
+ in your layout's `<head>`:
37
+
38
+ ```erb
39
+ <%= stylesheet_link_tag "ultimate_turbo_modal", "data-turbo-track": "reload" %>
40
+ ```
41
+
42
+ The gem puts the stylesheet on the asset pipeline's load path, so this works with
43
+ Propshaft and Sprockets whether you use importmaps or a JavaScript bundler. Keep it
44
+ *before* your own stylesheets: where the two collide at equal specificity the later one
45
+ wins, so loading the defaults first is what lets your own styles override them.
46
+
47
+ If you would rather pull the CSS through your bundler, the npm package also ships it:
48
+
49
+ ```css
50
+ @import 'ultimate_turbo_modal/dist/vanilla.css';
51
+ ```
52
+
29
53
 
30
54
  ## Usage
31
55
 
@@ -102,6 +126,7 @@ UltimateTurboModal.configure do |config|
102
126
  config.modal do |m|
103
127
  m.advance = false
104
128
  m.close_button = true
129
+ m.close_on_submit = true
105
130
  m.header = true
106
131
  m.header_divider = true
107
132
  m.footer_divider = true
@@ -111,7 +136,9 @@ UltimateTurboModal.configure do |config|
111
136
 
112
137
  config.drawer do |d|
113
138
  d.position = :right
139
+ d.advance = false
114
140
  d.close_button = true
141
+ d.close_on_submit = true
115
142
  d.header = true
116
143
  d.header_divider = false
117
144
  d.footer_divider = true
@@ -119,6 +146,14 @@ UltimateTurboModal.configure do |config|
119
146
  d.overlay = true
120
147
  d.size = :md
121
148
  end
149
+
150
+ # Only used when Turbo Confirm support is enabled.
151
+ # See https://github.com/cmer/ultimate_turbo_modal#turbo-confirm
152
+ config.confirm do |c|
153
+ c.title = "Are you sure?"
154
+ c.accept_label = "OK"
155
+ c.cancel_label = "Cancel"
156
+ end
122
157
  end
123
158
  ```
124
159
 
@@ -130,6 +165,7 @@ Per-instance options passed to `modal()` or `drawer()` override the defaults.
130
165
  |------|---------|-------------|
131
166
  | `advance` | `false` | When opening the modal, the URL in the URL bar will change to the URL of the view being shown in the modal. The Back button dismisses the modal and navigates back. If a URL is specified as a string (e.g. `advance: "/other-path"`), the browser history will advance, and the URL shown in the URL bar will be replaced with the value specified. |
132
167
  | `close_button` | `true` | Shows or hide a close button (X) at the top right of the modal. |
168
+ | `close_on_submit` | `true` | Whether a successful form submission dismisses the modal. See [Closing on form submission](#closing-on-form-submission). |
133
169
  | `header` | `true` | Whether to display a modal header. |
134
170
  | `header_divider` | `true` | Whether to display a divider below the header. |
135
171
  | `footer_divider` | `true` | Whether to display a divider above the footer. |
@@ -177,8 +213,10 @@ Link to it the same way as a modal:
177
213
  |------|---------|-------------|
178
214
  | `position` | `:right` | Which edge the drawer slides from. `:right` or `:left`. |
179
215
  | `size` | `:md` | Width of the drawer. One of `:xs`, `:sm`, `:md`, `:lg`, `:xl`, `:"2xl"`, `:full`, or a CSS string (e.g. `"500px"`). |
216
+ | `advance` | `false` | When opening the drawer, the URL in the URL bar will change to the URL of the view being shown in the drawer. The Back button dismisses the drawer and navigates back. If a URL is specified as a string (e.g. `advance: "/other-path"`), the browser history will advance, and the URL shown in the URL bar will be replaced with the value specified. |
180
217
  | `overlay` | `true` | Whether to show a backdrop overlay behind the drawer. |
181
218
  | `close_button` | `true` | Shows or hide a close button (X). |
219
+ | `close_on_submit` | `true` | Whether a successful form submission dismisses the drawer. See [Closing on form submission](#closing-on-form-submission). |
182
220
  | `header` | `true` | Whether to display a header. |
183
221
  | `header_divider` | `false` | Whether to display a divider below the header. |
184
222
  | `footer_divider` | `true` | Whether to display a divider above the footer. |
@@ -203,6 +241,215 @@ Link to it the same way as a modal:
203
241
  | CSS string | Custom value, e.g. `"500px"` or `"50vw"` |
204
242
 
205
243
 
244
+ ## Closing on form submission
245
+
246
+ By default, a form submitted inside a modal or drawer dismisses it once the
247
+ submission succeeds. Set `close_on_submit: false` to keep it open instead —
248
+ useful for chat composers, image uploaders, inline "add another" forms, and
249
+ anything else where the user is expected to submit repeatedly:
250
+
251
+ ```erb
252
+ <%= drawer(title: "Messages", close_on_submit: false) do %>
253
+ <%= render "messages/list" %>
254
+ <%= form_with model: Message.new do |f| %>
255
+ <%= f.text_field :body %>
256
+ <%= f.submit "Send" %>
257
+ <% end %>
258
+ <% end %>
259
+ ```
260
+
261
+ Respond with a Turbo Stream to update the contents in place, and send
262
+ `turbo_stream.modal(:close)` from the server on the submissions that *should*
263
+ dismiss it.
264
+
265
+ Two things are worth calling out:
266
+
267
+ - **Failed submissions never dismiss.** A 422 rendering validation errors leaves
268
+ the modal open regardless of this setting, so errors are shown in place.
269
+ - **Redirects still dismiss.** If the server redirects to a page that doesn't
270
+ contain the modal frame, the browser is navigating away and the modal closes
271
+ (smoothly) even with `close_on_submit: false`.
272
+
273
+ ### Per-form overrides
274
+
275
+ `data-modal-close-on-submit` on a form overrides the setting for that form
276
+ alone, so a single modal can mix both behaviors:
277
+
278
+ ```erb
279
+ <%= drawer(title: "Messages", close_on_submit: false) do %>
280
+ <%# Stays open — inherits close_on_submit: false %>
281
+ <%= form_with model: Message.new do |f| %>
282
+ <%= f.text_field :body %>
283
+ <%= f.submit "Send" %>
284
+ <% end %>
285
+
286
+ <%# Dismisses the drawer, despite close_on_submit: false %>
287
+ <%= form_with model: @conversation, method: :delete,
288
+ data: { modal_close_on_submit: true } do |f| %>
289
+ <%= f.submit "Delete conversation" %>
290
+ <% end %>
291
+ <% end %>
292
+ ```
293
+
294
+ It works in both directions: `data: { modal_close_on_submit: false }` on a form
295
+ inside a default modal keeps that one form from dismissing it. Placing the
296
+ attribute on a wrapping element applies it to every form inside; the nearest
297
+ one wins.
298
+
299
+ ## Turbo Confirm
300
+
301
+ Optional, opt-in. Once enabled, Turbo's `data-turbo-confirm` prompts render as
302
+ UTMR dialogs in your app's flavor instead of the browser's `window.confirm`.
303
+
304
+ Enabling it is one line in your layout:
305
+
306
+ ```erb
307
+ <%= modal_confirm_template %>
308
+ ```
309
+
310
+ That's the whole opt-in — no JavaScript changes. The helper renders an inert
311
+ `<template>`, and its presence on the page is the switch: with it, UTMR handles
312
+ confirmations; without it, Turbo falls back to `window.confirm` exactly as
313
+ before. Remove the line to turn the feature off.
314
+
315
+ You can also flip it from the initializer, which is handy for toggling per
316
+ environment without touching the layout:
317
+
318
+ ```ruby
319
+ UltimateTurboModal.configure do |config|
320
+ config.confirm do |c|
321
+ c.enabled = false
322
+ end
323
+ end
324
+ ```
325
+
326
+ ### Basic usage
327
+
328
+ Nothing changes about how you write confirmations:
329
+
330
+ ```erb
331
+ <%= button_to "Delete", post_path(post), method: :delete,
332
+ form: { data: { turbo_confirm: "This can't be undone." } } %>
333
+ ```
334
+
335
+ The message becomes the dialog body; the title and button labels come from your
336
+ configured defaults.
337
+
338
+ > [!NOTE]
339
+ > Turbo only runs confirmations for form submissions. A plain `<a>` needs
340
+ > `data-turbo-method` (or `data-turbo-stream`) for `data-turbo-confirm` to fire
341
+ > at all — that is Turbo's behavior, not UTMR's.
342
+
343
+ ### Customizing a single confirmation
344
+
345
+ For anything beyond the message, use the `modal_confirm` helper. It builds the
346
+ `data` attributes for you:
347
+
348
+ ```erb
349
+ <%= link_to "Delete", post_path(post), data: modal_confirm(
350
+ "This can't be undone.",
351
+ title: "Delete post?",
352
+ accept: "Delete",
353
+ variant: :danger,
354
+ turbo_method: :delete) %>
355
+ ```
356
+
357
+ | Option | Description |
358
+ |--------|-------------|
359
+ | `body` | First positional argument. The message. |
360
+ | `title` | Dialog heading. |
361
+ | `accept` | Label for the confirming button. |
362
+ | `cancel` | Label for the dismissing button. |
363
+ | `variant` | `:danger` styles the accept button destructively and moves the initial focus to Cancel. |
364
+ | `native` | `true` uses the browser's own `window.confirm` for this one prompt. |
365
+
366
+ Any option you leave out keeps its configured default.
367
+
368
+ You can also write the attributes by hand, which is convenient on forms:
369
+
370
+ ```erb
371
+ <%= button_to "Delete", post_path(post), method: :delete, form: { data: {
372
+ turbo_confirm: "This can't be undone.",
373
+ turbo_confirm_title: "Delete post?",
374
+ turbo_confirm_accept: "Delete",
375
+ turbo_confirm_variant: "danger" } } %>
376
+ ```
377
+
378
+ `data-turbo-confirm` itself has to be on the form or on the submit button:
379
+ Turbo looks nowhere else, and a prompt written on a wrapping element is simply
380
+ never triggered. The `data-turbo-confirm-*` options are read once the prompt has
381
+ fired, so those may also sit on a wrapping element. The submitter wins when both
382
+ it and the form carry the same option.
383
+
384
+ > [!IMPORTANT]
385
+ > On a **link**, use `modal_confirm`. Sibling `data-turbo-confirm-*` attributes
386
+ > do not survive: Turbo rewrites a link carrying `data-turbo-method` into a
387
+ > hidden form and copies only a fixed set of attributes across, so they are gone
388
+ > before UTMR is called. The helper's JSON payload rides inside
389
+ > `data-turbo-confirm` itself, which always survives. On forms, either style
390
+ > works.
391
+
392
+ ### Defaults
393
+
394
+ ```ruby
395
+ UltimateTurboModal.configure do |config|
396
+ config.confirm do |c|
397
+ c.enabled = true
398
+ c.title = "Are you sure?"
399
+ c.accept_label = "OK"
400
+ c.cancel_label = "Cancel"
401
+ c.close_button = false # a confirm has its own Cancel button
402
+ c.header_divider = false # dividers chop up one or two lines of text
403
+ c.footer_divider = false
404
+ c.header = true
405
+ c.padding = true
406
+ c.overlay = true
407
+ end
408
+ end
409
+ ```
410
+
411
+ The confirm dialog is deliberately plainer than a modal: no close button and no
412
+ dividers by default, and a set width (floor `20rem`, cap `28rem`) so a one-word
413
+ prompt and a three-line one come out the same size. Adjust it by overriding
414
+ `CONFIRM_CONTENT_CLASSES` in your flavor file.
415
+
416
+ ### Behavior
417
+
418
+ - The dialog is appended to `<body>`, so it layers above an open modal or drawer without any extra setup.
419
+ - **ESC** and the Cancel button both cancel — the action does not run.
420
+ - Clicking the backdrop does nothing. A confirm has to be answered, so a stray click never decides it.
421
+ - The confirming button is focused on open, so Enter accepts. `variant: :danger` focuses Cancel instead.
422
+ - Accepting waits for the close animation to finish before the request is sent.
423
+ - `window.modal` keeps pointing at the modal underneath, so `turbo_stream.modal(:close)` still addresses the right dialog.
424
+ - `data-turbo-confirm-native` on a form or submitter opts that one confirmation back out to `window.confirm`. On a link, pass `native: true` to `modal_confirm` instead, for the same reason the other options have to travel in the payload.
425
+ - If something else in your app assigns `Turbo.config.forms.confirm` after UTMR loads, it wins. Import `enableModalConfirm` from the package and call it afterwards to take the hook back.
426
+
427
+ ### Styling
428
+
429
+ The dialog is cloned from the template, so it shares your flavor's dialog,
430
+ backdrop and transition classes. Every slot falls back to its `MODAL_*` class
431
+ unless the flavor defines a `CONFIRM_*` override, so you only need to define
432
+ what should differ:
433
+
434
+ | Constant | Falls back to |
435
+ |----------|---------------|
436
+ | `CONFIRM_INNER_CLASSES` | `MODAL_INNER_CLASSES` |
437
+ | `CONFIRM_CONTENT_CLASSES` | `MODAL_CONTENT_CLASSES` |
438
+ | `CONFIRM_HEADER_CLASSES` | `MODAL_HEADER_CLASSES` |
439
+ | `CONFIRM_TITLE_CLASSES` | `MODAL_TITLE_CLASSES` |
440
+ | `CONFIRM_TITLE_H_CLASSES` | `MODAL_TITLE_H_CLASSES` |
441
+ | `CONFIRM_MAIN_CLASSES` | `MODAL_MAIN_CLASSES` |
442
+ | `CONFIRM_FOOTER_CLASSES` | `MODAL_FOOTER_CLASSES` |
443
+ | `CONFIRM_BODY_CLASSES` | — (confirm only) |
444
+ | `CONFIRM_ACTIONS_CLASSES` | — (confirm only) |
445
+ | `CONFIRM_ACCEPT_CLASSES` | — (confirm only) |
446
+ | `CONFIRM_CANCEL_CLASSES` | — (confirm only) |
447
+
448
+ Upgrading from an earlier version? Run
449
+ `rails generate ultimate_turbo_modal:update` to refresh your flavor file — until
450
+ you do, the confirm dialog falls back to the modal's classes and the four
451
+ confirm-only slots render unstyled rather than raising.
452
+
206
453
  ## Opening a Modal from a Drawer
207
454
 
208
455
  You don't need to do anything special. Use `data-turbo-frame="modal"` like you would anywhere else, and UTMR handles the rest:
@@ -262,6 +509,7 @@ For a full lifecycle walkthrough and edge-case notes, see [docs/modal-from-drawe
262
509
  - Automatic (or not) close button
263
510
  - Native focus trapping via the `<dialog>` element for improved accessibility (Tab and Shift+Tab cycle through focusable elements within the modal only)
264
511
  - Smooth redirects: form submissions that redirect back to the same page morph the content behind the modal before closing; redirects to a different page close the modal with animation first, then navigate
512
+ - Optional Turbo Confirm support: `data-turbo-confirm` prompts render as a styled dialog instead of the browser's `window.confirm`
265
513
 
266
514
 
267
515
  ### Running the Demo Application
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.2.1
1
+ 3.4.0