ultimate_turbo_modal 3.3.0 → 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: b52125720fe85438df47417fd9fc02a2d9509d3da42d1abbbe392892f5c0cc8a
4
- data.tar.gz: 67bad39477deb507e85a4fd2b481f4b7947c8eda4391c10c9d392d0935c50b85
3
+ metadata.gz: e8449cf081c6d1dfc2b84106cc16b316fb6a15b25031a2a9dc94c2ce4bf99b35
4
+ data.tar.gz: b1067b8a6ef21a15e9556bee1e8a0ceb45fdb453ff8fa772eda0ad5191b7d669
5
5
  SHA512:
6
- metadata.gz: '08478369a96054c23ad7c627da033febb05c0bbd40afe48e6ed54ed3d0098fb7efc175f152a4e72b21108cce21607d047d5f7956afbe8c64acdafd3cfadd50e7'
7
- data.tar.gz: 75f5307c6f8bf3fca81357bd0da3a5f9b5dca97aa977cd267bf91b9f5156ebe39643965b215e33ac1fd8671225385abf9674f90fa23309bc83208ea1d7ff1076
6
+ metadata.gz: 86b485906013944b475e4f0aece1ee980dd226aa160b8daed3628fb84b8b7cbbea8b015086310372f2fceab89d34372a18acd0b4cd76720dbf5af0cd18574f74
7
+ data.tar.gz: 5e6753fa5decf7401e07a2bda70dcef18a25fe56c2c487f0eb76e66dc010b556b52a2d7893201de73815431bd95600a80861de94e7758d863d3e98949d67783f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ## [3.3.0] - 2026-09-05
4
9
 
5
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`.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ultimate_turbo_modal (3.3.0)
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
@@ -146,6 +146,14 @@ UltimateTurboModal.configure do |config|
146
146
  d.overlay = true
147
147
  d.size = :md
148
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
149
157
  end
150
158
  ```
151
159
 
@@ -288,6 +296,160 @@ inside a default modal keeps that one form from dismissing it. Placing the
288
296
  attribute on a wrapping element applies it to every form inside; the nearest
289
297
  one wins.
290
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
+
291
453
  ## Opening a Modal from a Drawer
292
454
 
293
455
  You don't need to do anything special. Use `data-turbo-frame="modal"` like you would anywhere else, and UTMR handles the rest:
@@ -347,6 +509,7 @@ For a full lifecycle walkthrough and edge-case notes, see [docs/modal-from-drawe
347
509
  - Automatic (or not) close button
348
510
  - Native focus trapping via the `<dialog>` element for improved accessibility (Tab and Shift+Tab cycle through focusable elements within the modal only)
349
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`
350
513
 
351
514
 
352
515
  ### Running the Demo Application
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.3.0
1
+ 3.4.0
@@ -190,6 +190,160 @@ dialog.utmr {
190
190
  height: 1.25rem;
191
191
  }
192
192
 
193
+ /* ========================================
194
+ Confirm dialog styles
195
+ Scoped to [data-utmr-confirm] so it can reuse the modal container,
196
+ backdrop and enter/leave transitions while overriding the layout.
197
+ ======================================== */
198
+
199
+ &[data-utmr-confirm] {
200
+ /* Real gutter on mobile: the card below is full width. */
201
+ .modal-inner {
202
+ padding: 1rem;
203
+ padding-top: 10vh;
204
+
205
+ @media (min-width: 640px) {
206
+ padding: 1rem;
207
+ padding-bottom: 10vh;
208
+ }
209
+ }
210
+
211
+ /* Sized rather than shrink-to-fit, so a one-word prompt and a three-line
212
+ one come out the same. The floor is written as min(20rem, 100%) so it can
213
+ never push the card wider than the space it has on a narrow screen. */
214
+ .confirm-content {
215
+ position: relative;
216
+ overflow: hidden;
217
+ width: 100%;
218
+ min-width: min(20rem, 100%);
219
+ background-color: #ffffff;
220
+ text-align: left;
221
+ border-radius: 0.5rem;
222
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
223
+
224
+ @media (min-width: 640px) {
225
+ max-width: 28rem;
226
+ }
227
+ }
228
+
229
+ .confirm-header {
230
+ display: flex;
231
+ align-items: flex-start;
232
+ justify-content: space-between;
233
+ gap: 1rem;
234
+ width: 100%;
235
+ padding: 1.5rem 1.5rem 0;
236
+ }
237
+
238
+ .confirm-title-h {
239
+ font-size: 1rem;
240
+ line-height: 1.5rem;
241
+ font-weight: 600;
242
+ }
243
+
244
+ .confirm-main {
245
+ padding: 0;
246
+ }
247
+
248
+ .confirm-body {
249
+ font-size: 0.875rem;
250
+ line-height: 1.5rem;
251
+ }
252
+
253
+ .confirm-footer {
254
+ padding: 1.5rem;
255
+ }
256
+
257
+ /* Stacked and full width on mobile, inline and right aligned from 640px.
258
+ Reversed so the primary action sits on top. */
259
+ .confirm-actions {
260
+ display: flex;
261
+ width: 100%;
262
+ flex-direction: column-reverse;
263
+ gap: 0.5rem;
264
+
265
+ @media (min-width: 640px) {
266
+ flex-direction: row;
267
+ justify-content: flex-end;
268
+ gap: 0.75rem;
269
+ }
270
+ }
271
+
272
+ .confirm-cancel,
273
+ .confirm-accept {
274
+ display: inline-flex;
275
+ width: 100%;
276
+ justify-content: center;
277
+ align-items: center;
278
+ padding: 0.625rem 1rem;
279
+ font-size: 0.875rem;
280
+ line-height: 1.25rem;
281
+ font-weight: 600;
282
+ border-radius: 0.375rem;
283
+ cursor: pointer;
284
+ transition: background-color 150ms ease-in-out;
285
+
286
+ @media (min-width: 640px) {
287
+ width: auto;
288
+ padding: 0.5rem 1rem;
289
+ }
290
+ }
291
+
292
+ .confirm-cancel {
293
+ background-color: #ffffff;
294
+ color: #111827;
295
+ border: 1px solid #D1D5DB;
296
+
297
+ &:hover {
298
+ background-color: #F9FAFB;
299
+ }
300
+ }
301
+
302
+ .confirm-accept {
303
+ background-color: #4F46E5;
304
+ color: #ffffff;
305
+ border: 1px solid transparent;
306
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
307
+
308
+ &:hover {
309
+ background-color: #6366F1;
310
+ }
311
+ }
312
+
313
+ /* Destructive variant, set from `variant: :danger` */
314
+ &[data-utmr-confirm-variant="danger"] .confirm-accept {
315
+ background-color: #DC2626;
316
+
317
+ &:hover {
318
+ background-color: #EF4444;
319
+ }
320
+ }
321
+
322
+ /* Options, matching the modal's data-attribute switches */
323
+ &[data-header="false"] .confirm-header {
324
+ display: none;
325
+ }
326
+
327
+ &[data-title="false"] .confirm-title-h {
328
+ display: none;
329
+ }
330
+
331
+ &[data-padding="true"] .confirm-main {
332
+ padding: 0.5rem 1.5rem 0;
333
+ }
334
+
335
+ &[data-header-divider="true"] .confirm-header {
336
+ padding-bottom: 1rem;
337
+ border-bottom-width: 1px;
338
+ border-bottom-style: solid;
339
+ }
340
+
341
+ &[data-footer-divider="true"] .confirm-footer {
342
+ border-top-width: 1px;
343
+ border-top-style: solid;
344
+ }
345
+ }
346
+
193
347
  /* ========================================
194
348
  Drawer styles
195
349
  ======================================== */
@@ -399,6 +553,30 @@ dialog.utmr {
399
553
  color: #ffffff;
400
554
  }
401
555
 
556
+ .confirm-content {
557
+ background-color: #1F2937;
558
+ color: #ffffff;
559
+ }
560
+
561
+ .confirm-body {
562
+ color: #9CA3AF;
563
+ }
564
+
565
+ .confirm-header,
566
+ .confirm-footer {
567
+ border-color: #4B5563;
568
+ }
569
+
570
+ .confirm-cancel {
571
+ background-color: #374151;
572
+ color: #F3F4F6;
573
+ border-color: #4B5563;
574
+
575
+ &:hover {
576
+ background-color: #4B5563;
577
+ }
578
+ }
579
+
402
580
  .modal-close-button:hover,
403
581
  .modal-close-icon:hover,
404
582
  .drawer-close-button:hover,
@@ -409,6 +587,15 @@ dialog.utmr {
409
587
  }
410
588
 
411
589
  :not(.dark) dialog.utmr {
590
+ .confirm-body {
591
+ color: #6B7280;
592
+ }
593
+
594
+ .confirm-header,
595
+ .confirm-footer {
596
+ border-color: #E5E7EB;
597
+ }
598
+
412
599
  .modal-close-button,
413
600
  .modal-close-icon,
414
601
  .drawer-close-button,
@@ -127,6 +127,9 @@ module UltimateTurboModal
127
127
  def show_readme
128
128
  say "\nUltimateTurboModal installation complete!\n", :magenta
129
129
  say "Please review the initializer files, ensure JS is set up, and check your layout file.", :magenta
130
+ say "\nOptional: to render `data-turbo-confirm` prompts as modals instead of", :magenta
131
+ say "the browser's confirm dialog, add this to your layout:", :magenta
132
+ say " <%= modal_confirm_template %>", :cyan
130
133
  say "Don't forget to restart your Rails server!", :yellow
131
134
  end
132
135
 
@@ -8,7 +8,7 @@
8
8
  # Set to "" or omit entirely if you handle all styling via classes and external CSS.
9
9
  module UltimateTurboModal::Flavors
10
10
  class Custom < UltimateTurboModal::Base
11
- STYLES = "html:has(dialog#modal-container[open]) { overflow: hidden; }"
11
+ STYLES = "html:has(dialog.utmr[open]) { overflow: hidden; }"
12
12
 
13
13
  MODAL_DIALOG_CLASSES = ""
14
14
  MODAL_INNER_CLASSES = ""
@@ -23,6 +23,15 @@ module UltimateTurboModal::Flavors
23
23
  MODAL_CLOSE_SR_CLASSES = ""
24
24
  MODAL_CLOSE_ICON_CLASSES = ""
25
25
 
26
+ # Confirm dialog constants
27
+ # Only the slots that have no modal equivalent are listed. Every other slot
28
+ # (dialog, inner, content, header, title, main, footer) falls back to the
29
+ # matching MODAL_* class above; define a CONFIRM_* one to override it.
30
+ CONFIRM_BODY_CLASSES = ""
31
+ CONFIRM_ACTIONS_CLASSES = ""
32
+ CONFIRM_CANCEL_CLASSES = ""
33
+ CONFIRM_ACCEPT_CLASSES = ""
34
+
26
35
  # Drawer constants
27
36
  DRAWER_DIALOG_CLASSES = MODAL_DIALOG_CLASSES
28
37
  DRAWER_WRAPPER_CLASSES = ""
@@ -8,7 +8,7 @@
8
8
  # `data-entered` state (which would prevent the modal from animating out).
9
9
  module UltimateTurboModal::Flavors
10
10
  class Tailwind < UltimateTurboModal::Base
11
- STYLES = "html:has(dialog#modal-container[open]), html:has(dialog#modal-container-stacked[open]) { overflow: hidden; }"
11
+ STYLES = "html:has(dialog.utmr[open]) { overflow: hidden; }"
12
12
 
13
13
  # Modal constants
14
14
 
@@ -25,9 +25,11 @@ module UltimateTurboModal::Flavors
25
25
  "data-[closing]:backdrop:duration-200 data-[closing]:backdrop:ease-in"
26
26
  ].join(" ")
27
27
 
28
- MODAL_INNER_CLASSES = [
29
- "flex min-h-full items-start justify-center pt-[10vh] sm:items-center sm:pt-0 sm:p-4 sm:pb-[10vh]",
30
- # Transition
28
+ # The enter/leave animation, shared by the modal and the confirm dialog.
29
+ # Tailwind resolves conflicts by CSS source order rather than by the order
30
+ # classes appear in the string, so the two cannot share a layout line -- but
31
+ # they must not drift apart on the animation.
32
+ MODAL_TRANSITION_CLASSES = [
31
33
  "transition duration-300 ease-out",
32
34
  "group-data-[closing]/utmr-modal:duration-200 group-data-[closing]/utmr-modal:ease-in",
33
35
  # Default state (closed): faded + shifted
@@ -36,6 +38,11 @@ module UltimateTurboModal::Flavors
36
38
  "group-data-[entered]/utmr-modal:opacity-100 group-data-[entered]/utmr-modal:translate-y-0 group-data-[entered]/utmr-modal:scale-100"
37
39
  ].join(" ")
38
40
 
41
+ MODAL_INNER_CLASSES = [
42
+ "flex min-h-full items-start justify-center pt-[10vh] sm:items-center sm:pt-0 sm:p-4 sm:pb-[10vh]",
43
+ MODAL_TRANSITION_CLASSES
44
+ ].join(" ")
45
+
39
46
  MODAL_CONTENT_CLASSES = "relative transform max-h-screen overflow-hidden rounded-lg bg-white text-left shadow-lg transition-all sm:max-w-3xl dark:bg-gray-800 dark:text-white"
40
47
  MODAL_MAIN_CLASSES = "group-data-[padding=true]/utmr-modal:p-4 group-data-[padding=true]/utmr-modal:pt-2 overflow-y-auto max-h-[75vh]"
41
48
  MODAL_HEADER_CLASSES = "flex justify-between items-center w-full py-4 rounded-t dark:border-gray-600 group-data-[header-divider=true]/utmr-modal:border-b group-data-[header=false]/utmr-modal:absolute"
@@ -47,6 +54,73 @@ module UltimateTurboModal::Flavors
47
54
  MODAL_CLOSE_BUTTON_CLASSES = "text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
48
55
  MODAL_CLOSE_ICON_CLASSES = "w-5 h-5"
49
56
 
57
+ # Confirm dialog constants
58
+ #
59
+ # The confirm dialog renders through the modal's markup, so it inherits the
60
+ # dialog, backdrop and transition classes above. These constants only
61
+ # narrow the card and retune the spacing for what is a short prompt rather
62
+ # than a page of content. Any slot left undefined falls back to MODAL_*.
63
+
64
+ # Same transition as a modal, with a real gutter so the full-width card
65
+ # never touches the screen edge on mobile.
66
+ CONFIRM_INNER_CLASSES = [
67
+ "flex min-h-full items-start justify-center p-4 pt-[10vh] sm:items-center sm:pt-4 sm:pb-[10vh]",
68
+ MODAL_TRANSITION_CLASSES
69
+ ].join(" ")
70
+
71
+ # Sized rather than shrink-to-fit, so a one-word prompt and a three-line one
72
+ # come out the same. The floor is written as `min(20rem, 100%)` so it can
73
+ # never push the card wider than the space it has on a narrow screen.
74
+ CONFIRM_CONTENT_CLASSES = [
75
+ "relative w-full transform overflow-hidden rounded-lg text-left shadow-xl transition-all",
76
+ "min-w-[min(20rem,100%)] sm:max-w-md",
77
+ "bg-white dark:bg-gray-800 dark:text-white"
78
+ ].join(" ")
79
+
80
+ CONFIRM_HEADER_CLASSES = [
81
+ "flex items-start justify-between gap-4 w-full px-6 pt-6",
82
+ "dark:border-gray-600",
83
+ "group-data-[header-divider=true]/utmr-modal:border-b group-data-[header-divider=true]/utmr-modal:pb-4",
84
+ "group-data-[header=false]/utmr-modal:hidden"
85
+ ].join(" ")
86
+
87
+ CONFIRM_TITLE_CLASSES = "min-w-0"
88
+ CONFIRM_TITLE_H_CLASSES = "group-data-[title=false]/utmr-modal:hidden text-base font-semibold text-gray-900 dark:text-white"
89
+
90
+ CONFIRM_MAIN_CLASSES = "group-data-[padding=true]/utmr-modal:px-6 group-data-[padding=true]/utmr-modal:pt-2"
91
+ CONFIRM_BODY_CLASSES = "text-sm/6 text-gray-500 dark:text-gray-400"
92
+
93
+ CONFIRM_FOOTER_CLASSES = [
94
+ "px-6 pt-6 pb-6",
95
+ "dark:border-gray-600 group-data-[footer-divider=true]/utmr-modal:border-t"
96
+ ].join(" ")
97
+
98
+ # Stacked and full-width on mobile (thumb-friendly), inline and right
99
+ # aligned from `sm` up. Reversed so the primary action sits on top.
100
+ CONFIRM_ACTIONS_CLASSES = "flex w-full flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:gap-3"
101
+
102
+ CONFIRM_BUTTON_CLASSES = [
103
+ "inline-flex w-full sm:w-auto justify-center items-center rounded-md",
104
+ "px-4 py-2.5 sm:py-2 text-sm font-semibold transition-colors",
105
+ "focus-visible:outline-2 focus-visible:outline-offset-2"
106
+ ].join(" ")
107
+
108
+ CONFIRM_CANCEL_CLASSES = [
109
+ CONFIRM_BUTTON_CLASSES,
110
+ "bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50",
111
+ "dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600 dark:hover:bg-gray-600",
112
+ "focus-visible:outline-gray-400"
113
+ ].join(" ")
114
+
115
+ CONFIRM_ACCEPT_CLASSES = [
116
+ CONFIRM_BUTTON_CLASSES,
117
+ "text-white shadow-sm bg-indigo-600 hover:bg-indigo-500 focus-visible:outline-indigo-600",
118
+ # Destructive variant, set from `variant: :danger`
119
+ "group-data-[utmr-confirm-variant=danger]/utmr-modal:bg-red-600",
120
+ "group-data-[utmr-confirm-variant=danger]/utmr-modal:hover:bg-red-500",
121
+ "group-data-[utmr-confirm-variant=danger]/utmr-modal:focus-visible:outline-red-600"
122
+ ].join(" ")
123
+
50
124
  # Drawer constants
51
125
 
52
126
  DRAWER_DIALOG_CLASSES = [
@@ -5,7 +5,7 @@
5
5
  # Set STYLES to a CSS string if you need additional inline styles.
6
6
  module UltimateTurboModal::Flavors
7
7
  class Vanilla < UltimateTurboModal::Base
8
- STYLES = "html:has(dialog#modal-container[open]), html:has(dialog#modal-container-stacked[open]) { overflow: hidden; }"
8
+ STYLES = "html:has(dialog.utmr[open]) { overflow: hidden; }"
9
9
 
10
10
  MODAL_DIALOG_CLASSES = "modal-container"
11
11
  MODAL_INNER_CLASSES = "modal-inner"
@@ -20,6 +20,19 @@ module UltimateTurboModal::Flavors
20
20
  MODAL_CLOSE_SR_CLASSES = "sr-only"
21
21
  MODAL_CLOSE_ICON_CLASSES = "modal-close-icon"
22
22
 
23
+ # Confirm dialog constants
24
+ # Slots without a CONFIRM_* constant (dialog, inner) fall back to MODAL_*.
25
+ CONFIRM_CONTENT_CLASSES = "confirm-content"
26
+ CONFIRM_HEADER_CLASSES = "confirm-header"
27
+ CONFIRM_TITLE_CLASSES = "confirm-title"
28
+ CONFIRM_TITLE_H_CLASSES = "confirm-title-h"
29
+ CONFIRM_MAIN_CLASSES = "confirm-main"
30
+ CONFIRM_BODY_CLASSES = "confirm-body"
31
+ CONFIRM_FOOTER_CLASSES = "confirm-footer"
32
+ CONFIRM_ACTIONS_CLASSES = "confirm-actions"
33
+ CONFIRM_CANCEL_CLASSES = "confirm-cancel"
34
+ CONFIRM_ACCEPT_CLASSES = "confirm-accept"
35
+
23
36
  # Drawer constants
24
37
  DRAWER_DIALOG_CLASSES = "drawer-container"
25
38
  DRAWER_WRAPPER_CLASSES = "drawer-wrapper"
@@ -27,4 +27,19 @@ UltimateTurboModal.configure do |config|
27
27
  # d.overlay = true
28
28
  # d.size = :md
29
29
  # end
30
+
31
+ # Turbo Confirm support. Enable it by adding `<%= modal_confirm_template %>`
32
+ # to your layout; these are the dialog's defaults.
33
+ # config.confirm do |c|
34
+ # c.enabled = true
35
+ # c.title = "Are you sure?"
36
+ # c.accept_label = "OK"
37
+ # c.cancel_label = "Cancel"
38
+ # c.close_button = false
39
+ # c.header = true
40
+ # c.header_divider = false
41
+ # c.footer_divider = false
42
+ # c.padding = true
43
+ # c.overlay = true
44
+ # end
30
45
  end
@@ -4,6 +4,7 @@ class UltimateTurboModal::Base < Phlex::HTML
4
4
  prepend Phlex::DeferredRenderWithMainContent
5
5
 
6
6
  attr_accessor :request, :allowed_click_outside_selector, :content_div_data
7
+ CONFIRM_TEMPLATE_ID = "utmr-confirm-template"
7
8
  VALID_DRAWER_SIZES = %i[xs sm md lg xl 2xl full].freeze
8
9
  VALID_DRAWER_POSITIONS = %i[right left].freeze
9
10
 
@@ -13,6 +14,7 @@ class UltimateTurboModal::Base < Phlex::HTML
13
14
  # @param close_button_data_action [String] `data-action` attribute for the close button
14
15
  # @param close_button_sr_label [String] Close button label for screen readers
15
16
  # @param close_on_submit [Boolean] Whether to dismiss on a successful, non-redirecting form submission
17
+ # @param confirm [Boolean] Internal: render the Turbo Confirm dialog template. Use the `modal_confirm_template` view helper instead.
16
18
  # @param drawer_position [Symbol, false] Internal: drawer position (:right, :left) or false for standard modal. Use the `drawer()` view helper instead.
17
19
  # @param footer_divider [Boolean] Whether to show a divider between the main content and the footer
18
20
  # @param header [Boolean] Whether to show a modal header
@@ -30,6 +32,7 @@ class UltimateTurboModal::Base < Phlex::HTML
30
32
  close_button_data_action: "modal#hideModal",
31
33
  close_button_sr_label: "Close modal",
32
34
  close_on_submit: nil,
35
+ confirm: false,
33
36
  drawer_position: false,
34
37
  footer_divider: nil,
35
38
  header: nil,
@@ -38,14 +41,23 @@ class UltimateTurboModal::Base < Phlex::HTML
38
41
  padding: nil,
39
42
  size: nil,
40
43
  content_div_data: nil,
44
+ accept_label: nil,
45
+ cancel_label: nil,
41
46
  request: nil, title: nil
42
47
  )
43
48
  @drawer = drawer_position
49
+ @confirm = confirm
44
50
  @request = request
45
51
 
46
52
  raise ArgumentError, "Cannot render a drawer into the drawer-modal frame (drawers cannot be opened from inside another drawer or modal)" if drawer? && stacked?
47
53
 
48
- if drawer?
54
+ if confirm?
55
+ cfg = UltimateTurboModal.configuration.confirm_config
56
+ @drawer_size = nil
57
+ @accept_label = accept_label || cfg.accept_label
58
+ @cancel_label = cancel_label || cfg.cancel_label
59
+ title ||= cfg.title
60
+ elsif drawer?
49
61
  cfg = UltimateTurboModal.configuration.drawer_config
50
62
  @drawer_size = self.class.validate_drawer_size!(size || cfg.size)
51
63
  else
@@ -90,7 +102,11 @@ class UltimateTurboModal::Base < Phlex::HTML
90
102
  end
91
103
 
92
104
  def view_template(&block)
93
- if turbo_frame?
105
+ if confirm?
106
+ # Inert until the JS clones it, so it can live in the layout on every
107
+ # page without a dialog (or its <style> tag) ever being active.
108
+ template(id: CONFIRM_TEMPLATE_ID) { render_confirm }
109
+ elsif turbo_frame?
94
110
  turbo_frame_tag(turbo_frame_name) do
95
111
  drawer? ? render_drawer(&block) : render_modal(&block)
96
112
  end
@@ -103,12 +119,16 @@ class UltimateTurboModal::Base < Phlex::HTML
103
119
  stacked? ? "drawer-modal" : "modal"
104
120
  end
105
121
 
122
+ # Both return nil so `<%= m.title do %>` writes nothing to the ERB buffer.
123
+ # The assignment's own value is a Proc, and ERB would print it.
106
124
  def title(&block)
107
125
  @title_block = block
126
+ nil
108
127
  end
109
128
 
110
129
  def footer(&block)
111
130
  @footer = block
131
+ nil
112
132
  end
113
133
 
114
134
  class << self
@@ -136,13 +156,16 @@ class UltimateTurboModal::Base < Phlex::HTML
136
156
 
137
157
  def close_on_submit? = !!@close_on_submit
138
158
 
159
+ def confirm? = !!@confirm
160
+
139
161
  def title_block? = !!@title_block
140
162
 
141
163
  def title? = !!@title
142
164
 
143
165
  def header? = !!@header
144
166
 
145
- def footer? = @footer.present?
167
+ # A confirm's footer holds its own buttons rather than a user block.
168
+ def footer? = @footer.present? || confirm?
146
169
 
147
170
  def header_divider? = !!@header_divider && (@title_block.present? || title?)
148
171
 
@@ -168,13 +191,22 @@ class UltimateTurboModal::Base < Phlex::HTML
168
191
  frame == "drawer-modal" || frame == "modal-inner-stacked"
169
192
  end
170
193
 
171
- def dialog_id = stacked? ? "modal-container-stacked" : "modal-container"
194
+ def dialog_id = scoped_id("modal-container")
172
195
 
173
- def inner_id = stacked? ? "modal-inner-stacked" : "modal-inner"
196
+ def inner_id = scoped_id("modal-inner")
174
197
 
175
198
  # Suffix inner ids so they don't collide with the drawer's ids when the
176
- # stacked modal is rendered inside the drawer's DOM.
177
- def scoped_id(name) = stacked? ? "#{name}-stacked" : name
199
+ # stacked modal is rendered inside the drawer's DOM, or with an open
200
+ # modal's ids when the confirm dialog is layered over it.
201
+ def scoped_id(name) = "#{name}#{id_suffix}"
202
+
203
+ def id_suffix
204
+ return "-confirm" if confirm?
205
+ # A layout renders for Turbo Frame requests too, so this has to be checked
206
+ # after confirm? -- otherwise a confirm template rendered during a stacked
207
+ # request would claim the stacked modal's ids.
208
+ stacked? ? "-stacked" : ""
209
+ end
178
210
 
179
211
  def drawer_position = @drawer || :right
180
212
 
@@ -252,11 +284,42 @@ class UltimateTurboModal::Base < Phlex::HTML
252
284
  raw(safe(str))
253
285
  end
254
286
 
287
+ # Confirm dialogs render through the modal's markup, so every slot falls back
288
+ # to its MODAL_* class unless the flavor defines a CONFIRM_* override.
255
289
  def classes_for(suffix)
290
+ if confirm?
291
+ override = confirm_classes_for(suffix)
292
+ return override unless override.nil?
293
+ end
294
+
256
295
  prefix = drawer? ? "DRAWER" : "MODAL"
257
296
  self.class.const_get("#{prefix}_#{suffix}")
258
297
  end
259
298
 
299
+ # CONFIRM_* constants are optional. A flavor file written before confirm
300
+ # support returns nil here, which means shared slots fall back to MODAL_* and
301
+ # the confirm-only slots (body, actions, buttons) render unstyled rather than
302
+ # raising. `warn_missing_confirm_classes` is what tells the developer why.
303
+ def confirm_classes_for(suffix)
304
+ const = :"CONFIRM_#{suffix}"
305
+ self.class.const_defined?(const) ? self.class.const_get(const) : nil
306
+ end
307
+
308
+ # An unstyled confirm dialog looks like a bug in the gem rather than a stale
309
+ # flavor file, so say which it is. Once per flavor class, in development only.
310
+ def warn_missing_confirm_classes
311
+ return unless defined?(Rails) && Rails.env.local?
312
+ return if self.class.const_defined?(:CONFIRM_ACTIONS_CLASSES)
313
+ return if self.class.instance_variable_get(:@utmr_confirm_classes_warned)
314
+
315
+ self.class.instance_variable_set(:@utmr_confirm_classes_warned, true)
316
+ Rails.logger&.warn(
317
+ "[UltimateTurboModal] #{self.class.name} defines no CONFIRM_* classes, so the " \
318
+ "confirm dialog will render unstyled. Run `rails g ultimate_turbo_modal:update` " \
319
+ "to refresh your flavor file."
320
+ )
321
+ end
322
+
260
323
  def custom_drawer_size?
261
324
  @drawer_size.present? && !VALID_DRAWER_SIZES.include?(@drawer_size.to_s.to_sym)
262
325
  end
@@ -265,23 +328,35 @@ class UltimateTurboModal::Base < Phlex::HTML
265
328
  data_attributes = {
266
329
  controller: "modal",
267
330
  modal_target: "container",
268
- modal_advance_url_value: advance_url,
269
- modal_allowed_click_outside_selector_value: allowed_click_outside_selector,
270
- modal_close_on_submit_value: close_on_submit?.to_s,
271
- action: "turbo:submit-end->modal#submitEnd cancel->modal#cancelEvent mousedown->modal#dialogMousedown click->modal#dialogClicked",
331
+ action: dialog_actions,
272
332
  padding: padding?.to_s,
273
333
  title: title?.to_s,
274
334
  header: header?.to_s,
275
335
  close_button: close_button?.to_s,
276
336
  header_divider: header_divider?.to_s,
277
- footer_divider: footer_divider?.to_s
337
+ footer_divider: footer_divider?.to_s,
338
+ overlay: overlay?.to_s
278
339
  }
279
340
 
341
+ role = nil
342
+ aria_attributes = {labelledby: scoped_id("modal-title-h")}
343
+
344
+ if confirm?
345
+ # Everything the controller reads from those three values is driven by an
346
+ # action a confirm doesn't bind, so they would only ever mislead a reader.
347
+ data_attributes[:utmr_confirm] = ""
348
+ role = "alertdialog"
349
+ aria_attributes[:describedby] = scoped_id("modal-body")
350
+ else
351
+ data_attributes[:modal_advance_url_value] = advance_url
352
+ data_attributes[:modal_allowed_click_outside_selector_value] = allowed_click_outside_selector
353
+ data_attributes[:modal_close_on_submit_value] = close_on_submit?.to_s
354
+ end
355
+
280
356
  if drawer?
281
357
  data_attributes[:drawer] = drawer_position.to_s
282
358
  data_attributes[:drawer_size] = (@drawer_size.presence || "md").to_s
283
359
  end
284
- data_attributes[:overlay] = overlay?.to_s
285
360
 
286
361
  if defined?(Rails) && Rails.env.local?
287
362
  data_attributes[:utmr_version] = UltimateTurboModal::VERSION
@@ -297,17 +372,29 @@ class UltimateTurboModal::Base < Phlex::HTML
297
372
  dialog(id: dialog_id,
298
373
  class: dialog_classes,
299
374
  style: inline_style,
300
- aria: {
301
- labelledby: scoped_id("modal-title-h")
302
- },
375
+ role: role,
376
+ aria: aria_attributes,
303
377
  data: data_attributes, &block)
304
378
  end
305
379
 
380
+ # A confirm is a decision, so a stray click on the backdrop must not answer it
381
+ # for the user. Leaving the outside-click actions off is the whole of it --
382
+ # Escape and the Cancel button still dismiss.
383
+ def dialog_actions
384
+ return "cancel->modal#cancelEvent" if confirm?
385
+
386
+ ["turbo:submit-end->modal#submitEnd",
387
+ "cancel->modal#cancelEvent",
388
+ "mousedown->modal#dialogMousedown",
389
+ "click->modal#dialogClicked"].join(" ")
390
+ end
391
+
306
392
  ## Modal-specific elements
307
393
 
308
394
  def modal_inner(&block)
309
395
  maybe_turbo_frame(inner_id) do
310
- div(id: inner_id, class: self.class::MODAL_INNER_CLASSES, &block)
396
+ div(id: inner_id, class: self.class::MODAL_INNER_CLASSES,
397
+ data: {modal_target: "transition"}, &block)
311
398
  end
312
399
  end
313
400
 
@@ -336,7 +423,8 @@ class UltimateTurboModal::Base < Phlex::HTML
336
423
  end
337
424
 
338
425
  def drawer_panel(&block)
339
- div(id: "drawer-panel", class: self.class::DRAWER_PANEL_CLASSES, data: {modal_target: "content"}, &block)
426
+ div(id: "drawer-panel", class: self.class::DRAWER_PANEL_CLASSES,
427
+ data: {modal_target: "content transition"}, &block)
340
428
  end
341
429
 
342
430
  def drawer_content(&block)
@@ -354,6 +442,51 @@ class UltimateTurboModal::Base < Phlex::HTML
354
442
  end
355
443
  end
356
444
 
445
+ ## Confirm-specific elements
446
+
447
+ def render_confirm
448
+ warn_missing_confirm_classes
449
+ # Unlike the modal and drawer templates, the styles go *inside* the dialog:
450
+ # the JS clones this one node out of the <template>, and the scroll-lock
451
+ # rule has to come along with it and leave with it.
452
+ dialog_element do
453
+ styles
454
+ confirm_inner do
455
+ confirm_content do
456
+ render_header
457
+ confirm_main
458
+ confirm_footer
459
+ end
460
+ end
461
+ end
462
+ end
463
+
464
+ def confirm_inner(&block)
465
+ div(id: inner_id, class: classes_for("INNER_CLASSES"), data: {modal_target: "transition"}, &block)
466
+ end
467
+
468
+ def confirm_content(&block)
469
+ div(id: scoped_id("modal-content"), class: classes_for("CONTENT_CLASSES"), data: {modal_target: "content"}, &block)
470
+ end
471
+
472
+ # Left empty: the JavaScript fills it from `data-turbo-confirm`.
473
+ def confirm_main
474
+ render_main do
475
+ div(id: scoped_id("modal-body"), class: confirm_classes_for("BODY_CLASSES"))
476
+ end
477
+ end
478
+
479
+ def confirm_footer
480
+ div(id: scoped_id("modal-footer"), class: classes_for("FOOTER_CLASSES")) do
481
+ div(class: confirm_classes_for("ACTIONS_CLASSES")) do
482
+ button(type: "button", class: confirm_classes_for("CANCEL_CLASSES"),
483
+ data: {utmr_confirm_action: "cancel"}) { @cancel_label }
484
+ button(type: "button", class: confirm_classes_for("ACCEPT_CLASSES"),
485
+ data: {utmr_confirm_action: "accept"}) { @accept_label }
486
+ end
487
+ end
488
+ end
489
+
357
490
  ## Shared rendering
358
491
 
359
492
  def render_main(&block)
@@ -363,10 +496,20 @@ class UltimateTurboModal::Base < Phlex::HTML
363
496
  def render_header
364
497
  div(id: scoped_id("modal-header"), class: classes_for("HEADER_CLASSES")) do
365
498
  render_title
366
- drawer? ? drawer_close : modal_close
499
+ render_header_close
367
500
  end
368
501
  end
369
502
 
503
+ # Modals and drawers always render the close button and let the flavor hide it
504
+ # through `data-close-button`. A confirm leaves it out of the markup entirely
505
+ # instead, which keeps it out of the tab order and the accessibility tree --
506
+ # it has its own Cancel button, so it is off by default.
507
+ def render_header_close
508
+ return if confirm? && !close_button?
509
+
510
+ drawer? ? drawer_close : modal_close
511
+ end
512
+
370
513
  def render_title
371
514
  div(id: scoped_id("modal-title"), class: classes_for("TITLE_CLASSES")) do
372
515
  if @title_block.present?
@@ -14,7 +14,7 @@ module UltimateTurboModal
14
14
  :allowed_click_outside_selector, :allowed_click_outside_selector=, to: :configuration
15
15
 
16
16
  class Configuration
17
- attr_reader :flavor, :modal_config, :drawer_config
17
+ attr_reader :flavor, :modal_config, :drawer_config, :confirm_config
18
18
  attr_accessor :allowed_click_outside_selector
19
19
 
20
20
  def initialize
@@ -22,6 +22,7 @@ module UltimateTurboModal
22
22
  @allowed_click_outside_selector = []
23
23
  @modal_config = ModalConfig.new
24
24
  @drawer_config = DrawerConfig.new
25
+ @confirm_config = ConfirmConfig.new
25
26
  end
26
27
 
27
28
  def modal
@@ -34,6 +35,11 @@ module UltimateTurboModal
34
35
  @drawer_config
35
36
  end
36
37
 
38
+ def confirm
39
+ yield(@confirm_config) if block_given?
40
+ @confirm_config
41
+ end
42
+
37
43
  def flavor=(flavor)
38
44
  raise ArgumentError, "Value must be a symbol." unless flavor.is_a?(Symbol) || flavor.is_a?(String)
39
45
  @flavor = flavor.to_sym
@@ -87,6 +93,32 @@ module UltimateTurboModal
87
93
  end
88
94
  end
89
95
 
96
+ # Defaults for the Turbo Confirm dialog. Every value here is a fallback:
97
+ # anything supplied per-element through `data-turbo-confirm` wins.
98
+ class ConfirmConfig < BaseConfig
99
+ attr_accessor :title, :accept_label, :cancel_label
100
+ attr_reader :enabled
101
+
102
+ boolean_option :enabled
103
+
104
+ # `advance` and `close_on_submit` are left unset: a confirm never pushes a
105
+ # URL and holds no form, and the dialog binds neither behavior.
106
+ def initialize
107
+ @enabled = true
108
+ # A confirm has its own Cancel button, and dividers chop up what is
109
+ # only ever a line or two of text.
110
+ @close_button = false
111
+ @header = true
112
+ @header_divider = false
113
+ @footer_divider = false
114
+ @padding = true
115
+ @overlay = true
116
+ @title = "Are you sure?"
117
+ @accept_label = "OK"
118
+ @cancel_label = "Cancel"
119
+ end
120
+ end
121
+
90
122
  class DrawerConfig < BaseConfig
91
123
  attr_reader :size, :position
92
124
 
@@ -12,5 +12,36 @@ module UltimateTurboModal::Helpers
12
12
  size = UltimateTurboModal::Base.validate_drawer_size!(size || cfg.size)
13
13
  modal(drawer_position: position, size: size, **options, &block)
14
14
  end
15
+
16
+ # Renders the inert <template> the Turbo Confirm dialog is cloned from.
17
+ # Place it once in your layout: its presence is what enables the feature.
18
+ # Renders nothing when `config.confirm.enabled` is false, which turns the
19
+ # feature off without touching the layout.
20
+ def modal_confirm_template(**options)
21
+ return unless UltimateTurboModal.configuration.confirm_config.enabled
22
+
23
+ render(UltimateTurboModal.confirm(request:, **options))
24
+ end
25
+
26
+ # Builds the `data` attributes for an element that should prompt before
27
+ # acting. The options are JSON-encoded into `data-turbo-confirm` because
28
+ # Turbo rewrites a link with `data-turbo-method` into a synthetic form and
29
+ # copies only a fixed set of attributes across -- sibling
30
+ # `data-turbo-confirm-*` attributes on a link would be dropped.
31
+ #
32
+ # link_to "Delete", post_path(post), data: modal_confirm(
33
+ # "This can't be undone.", title: "Delete post?", accept: "Delete",
34
+ # variant: :danger, turbo_method: :delete)
35
+ #
36
+ # `native: true` opts this one confirmation back out to `window.confirm`.
37
+ # On a link that is the only way to say it, since Turbo drops sibling
38
+ # `data-turbo-confirm-native` along with everything else.
39
+ def modal_confirm(body = nil, title: nil, accept: nil, cancel: nil, variant: nil, native: nil, **data)
40
+ payload = {title: title, body: body, accept: accept, cancel: cancel,
41
+ variant: variant, native: native&.to_s}.compact
42
+ raise ArgumentError, "modal_confirm requires a body or at least one option" if payload.empty?
43
+
44
+ {turbo_confirm: payload.to_json}.merge(data)
45
+ end
15
46
  end
16
47
  end
@@ -16,6 +16,10 @@ module UltimateTurboModal
16
16
  modal_class.new(**)
17
17
  end
18
18
 
19
+ def confirm(**)
20
+ modal_class.new(confirm: true, **)
21
+ end
22
+
19
23
  def modal_class
20
24
  "UltimateTurboModal::Flavors::#{flavor.to_s.classify}".constantize
21
25
  rescue NameError
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ultimate_turbo_modal
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.0
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carl Mercier