ultimate_turbo_modal 2.1.2 → 2.2.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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.tool-versions +1 -1
  3. data/CHANGELOG.md +11 -0
  4. data/CLAUDE.md +188 -2
  5. data/Gemfile.lock +1 -1
  6. data/README.md +22 -0
  7. data/VERSION +1 -1
  8. data/javascript/index.js +12 -3
  9. data/javascript/modal_controller.js +11 -3
  10. data/javascript/package.json +2 -1
  11. data/javascript/styles/vanilla.css +86 -0
  12. data/lib/generators/ultimate_turbo_modal/templates/flavors/custom.rb +29 -2
  13. data/lib/generators/ultimate_turbo_modal/templates/flavors/tailwind.rb +30 -3
  14. data/lib/generators/ultimate_turbo_modal/templates/flavors/tailwind3.rb +29 -2
  15. data/lib/generators/ultimate_turbo_modal/templates/flavors/vanilla.rb +31 -3
  16. data/lib/generators/ultimate_turbo_modal/update_generator.rb +97 -0
  17. data/lib/ultimate_turbo_modal/base.rb +34 -13
  18. data/lib/ultimate_turbo_modal.rb +1 -0
  19. data/script/build_and_release.sh +20 -7
  20. metadata +7 -99
  21. data/demo-app/.gitattributes +0 -7
  22. data/demo-app/.gitignore +0 -34
  23. data/demo-app/.ruby-version +0 -1
  24. data/demo-app/.tool-versions +0 -2
  25. data/demo-app/Gemfile +0 -41
  26. data/demo-app/Gemfile.lock +0 -248
  27. data/demo-app/Procfile.dev +0 -3
  28. data/demo-app/README.md +0 -15
  29. data/demo-app/Rakefile +0 -6
  30. data/demo-app/app/assets/builds/.keep +0 -0
  31. data/demo-app/app/assets/images/.keep +0 -0
  32. data/demo-app/app/assets/stylesheets/application.css +0 -42
  33. data/demo-app/app/controllers/application_controller.rb +0 -10
  34. data/demo-app/app/controllers/concerns/.keep +0 -0
  35. data/demo-app/app/controllers/concerns/set_flavor.rb +0 -10
  36. data/demo-app/app/controllers/hide_from_backends_controller.rb +0 -34
  37. data/demo-app/app/controllers/modal_controller.rb +0 -11
  38. data/demo-app/app/controllers/posts_controller.rb +0 -69
  39. data/demo-app/app/controllers/welcome_controller.rb +0 -2
  40. data/demo-app/app/helpers/application_helper.rb +0 -9
  41. data/demo-app/app/javascript/application.js +0 -4
  42. data/demo-app/app/javascript/controllers/application.js +0 -13
  43. data/demo-app/app/javascript/controllers/dark_mode_controller.js +0 -28
  44. data/demo-app/app/javascript/controllers/flash_controller.js +0 -9
  45. data/demo-app/app/javascript/controllers/hello_controller.js +0 -7
  46. data/demo-app/app/javascript/controllers/index.js +0 -15
  47. data/demo-app/app/models/application_record.rb +0 -3
  48. data/demo-app/app/models/concerns/.keep +0 -0
  49. data/demo-app/app/models/post.rb +0 -4
  50. data/demo-app/app/views/hide_from_backends/_notice.html.erb +0 -10
  51. data/demo-app/app/views/hide_from_backends/create.turbo_stream.erb +0 -2
  52. data/demo-app/app/views/hide_from_backends/new.html.erb +0 -30
  53. data/demo-app/app/views/layouts/application.html.erb +0 -40
  54. data/demo-app/app/views/modal/index.html.erb +0 -45
  55. data/demo-app/app/views/modal/show.html.erb +0 -39
  56. data/demo-app/app/views/posts/_form.html.erb +0 -36
  57. data/demo-app/app/views/posts/_post.html.erb +0 -9
  58. data/demo-app/app/views/posts/edit.html.erb +0 -9
  59. data/demo-app/app/views/posts/index.html.erb +0 -14
  60. data/demo-app/app/views/posts/new.html.erb +0 -3
  61. data/demo-app/app/views/posts/show.html.erb +0 -21
  62. data/demo-app/app/views/shared/_flash.html.erb +0 -13
  63. data/demo-app/app/views/welcome/index.html.erb +0 -19
  64. data/demo-app/bin/bundle +0 -109
  65. data/demo-app/bin/dev +0 -18
  66. data/demo-app/bin/rails +0 -4
  67. data/demo-app/bin/rake +0 -4
  68. data/demo-app/bin/setup +0 -34
  69. data/demo-app/config/application.rb +0 -43
  70. data/demo-app/config/boot.rb +0 -3
  71. data/demo-app/config/credentials.yml.enc +0 -1
  72. data/demo-app/config/database.yml +0 -25
  73. data/demo-app/config/environment.rb +0 -14
  74. data/demo-app/config/environments/development.rb +0 -51
  75. data/demo-app/config/environments/production.rb +0 -67
  76. data/demo-app/config/environments/test.rb +0 -42
  77. data/demo-app/config/initializers/assets.rb +0 -7
  78. data/demo-app/config/initializers/content_security_policy.rb +0 -25
  79. data/demo-app/config/initializers/filter_parameter_logging.rb +0 -8
  80. data/demo-app/config/initializers/inflections.rb +0 -14
  81. data/demo-app/config/initializers/new_framework_defaults_8_0.rb +0 -30
  82. data/demo-app/config/initializers/permissions_policy.rb +0 -11
  83. data/demo-app/config/initializers/ultimate_turbo_modal.rb +0 -12
  84. data/demo-app/config/initializers/ultimate_turbo_modal_tailwind.rb +0 -21
  85. data/demo-app/config/initializers/ultimate_turbo_modal_vanilla.rb +0 -21
  86. data/demo-app/config/locales/en.yml +0 -33
  87. data/demo-app/config/puma.rb +0 -41
  88. data/demo-app/config/routes.rb +0 -9
  89. data/demo-app/config.ru +0 -6
  90. data/demo-app/db/migrate/20230331002502_create_posts.rb +0 -10
  91. data/demo-app/db/migrate/20231031012703_add_post.rb +0 -9
  92. data/demo-app/db/migrate/20231128141054_add_publish_on_to_posts.rb +0 -5
  93. data/demo-app/db/schema.rb +0 -22
  94. data/demo-app/lib/assets/.keep +0 -0
  95. data/demo-app/lib/tasks/.keep +0 -0
  96. data/demo-app/log/.keep +0 -0
  97. data/demo-app/package.json +0 -28
  98. data/demo-app/postcss.config.js +0 -7
  99. data/demo-app/public/400.html +0 -114
  100. data/demo-app/public/404.html +0 -114
  101. data/demo-app/public/406-unsupported-browser.html +0 -114
  102. data/demo-app/public/422.html +0 -114
  103. data/demo-app/public/500.html +0 -114
  104. data/demo-app/public/apple-touch-icon-precomposed.png +0 -0
  105. data/demo-app/public/apple-touch-icon.png +0 -0
  106. data/demo-app/public/favicon.ico +0 -0
  107. data/demo-app/public/icon.png +0 -0
  108. data/demo-app/public/icon.svg +0 -3
  109. data/demo-app/public/img/bootstrap-logo-shadow.png +0 -0
  110. data/demo-app/public/img/unicat.jpg +0 -0
  111. data/demo-app/public/img/vanilla.png +0 -0
  112. data/demo-app/public/robots.txt +0 -1
  113. data/demo-app/tmp/.keep +0 -0
  114. data/demo-app/tmp/pids/.keep +0 -0
  115. data/demo-app/vendor/.keep +0 -0
  116. data/demo-app/yarn.lock +0 -1035
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17d0eab498c9c828467bea1e33c1250763a8361595847a29a421bedc84947f06
4
- data.tar.gz: 41ab99f7579857ccd65d7c549d8626d2566fb952cabb5c97b7f8193e7930fa44
3
+ metadata.gz: f48f99a3169d751eb02f9efe0f357aa70b285097e03438e2d1b27d1693b9b59c
4
+ data.tar.gz: c333e8c524f3c2967d1ddc484aab887165f574e0969bdd1b1aa6d1a48752b7ba
5
5
  SHA512:
6
- metadata.gz: e4925980e51ac9f8fc7e08245e8d79e3a73faca356e44ae45bedfd089e3ec3d2de17e2a5ab264f57dcae0c79b91a53e37ee3661dcb57541b422b563f3e2a8ff1
7
- data.tar.gz: 93f0bb944adfcc54e844ce3d2d3438bdfdaabb3cd648bb22b87937a6b8267a46e842db622ffc7c04c7b9a2a578da3ea1c4c411742f44821368c4fd299a1c411f
6
+ metadata.gz: be4aa9c324429196062c98fd8845b484eb2f14f76e96ee2581063b05568f287e3e4f1961e55a5e6d11a2043a3785af13ebaaf1d9f6594abeb3750f785621f631
7
+ data.tar.gz: aaad99d25ccbd2bceb518a1a85ecf2e28a7e768e61ff478fddfc723aad170ef06cd7f325fc012e405008799f43f90600440cff93e29528c81cfcbb07b8c94184
data/.tool-versions CHANGED
@@ -1,2 +1,2 @@
1
- ruby 3.4.4
1
+ ruby 3.3.9
2
2
  nodejs 22.14.0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## [2.2.1] - 2025-08-08
2
+
3
+ - Added `rails generate ultimate_turbo_modal:update` for easy updates
4
+ - Exclude demo-app directory from gem package
5
+
6
+ ## [2.2.0] - 2025-08-07
7
+
8
+ - BREAKING: Make sure to re-run the generator `rails generate ultimate_turbo_modal:install` after install.
9
+ - Fixed transistions that were sometimes not showing
10
+ - Improved demo app to make it easier to use for development
11
+
1
12
  ## [2.1.2] - 2025-08-06
2
13
 
3
14
  - Fixed scroll lock
data/CLAUDE.md CHANGED
@@ -4,7 +4,187 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
 
5
5
  ## Overview
6
6
 
7
- Ultimate Turbo Modal (UTMR) is a full-featured modal implementation for Rails applications using Turbo, Stimulus, and Hotwire. It consists of both a Ruby gem and an npm package that work together.
7
+ Ultimate Turbo Modal (UTMR) is a full-featured modal implementation for Rails applications using Turbo, Stimulus, and Hotwire. It consists of both a Ruby gem and an npm package that work together to provide seamless modal functionality with proper focus management, history manipulation, and customizable styling.
8
+
9
+ ## Architecture
10
+
11
+ ### High-Level Design
12
+
13
+ The system follows a separation of concerns between server-side rendering (Ruby/Rails) and client-side behavior (JavaScript/Stimulus):
14
+
15
+ 1. **Server-Side (Ruby Gem)**: Handles HTML generation, configuration management, and Rails integration
16
+ 2. **Client-Side (JavaScript Package)**: Manages modal behavior, focus trapping, scroll locking, and Turbo interactions
17
+ 3. **Communication Layer**: Uses Turbo Frames, Turbo Streams, and data attributes to coordinate between server and client
18
+
19
+ ### Core Components
20
+
21
+ #### Ruby Gem Architecture
22
+
23
+ - **Module Structure**: `UltimateTurboModal` is the main module that delegates to configuration and instantiates modal classes
24
+ - **Base Class**: `UltimateTurboModal::Base` extends `Phlex::HTML` for component-based HTML generation
25
+ - **Configuration System**: Centralized configuration with validation and type checking
26
+ - **Flavor System**: CSS framework-specific implementations (Tailwind, Vanilla, Custom) that define styling classes
27
+ - **Rails Integration**: Via Railtie that injects helpers into ActionController and ActionView
28
+
29
+ #### JavaScript Architecture
30
+
31
+ - **Stimulus Controller**: `modal_controller.js` handles all modal interactions
32
+ - **Dependencies**:
33
+ - `el-transition`: For smooth enter/leave animations
34
+ - `focus-trap`: For accessibility-compliant focus management
35
+ - `idiomorph`: For intelligent DOM morphing to prevent flicker
36
+ - **Global Registration**: Modal instance exposed as `window.modal` for programmatic access
37
+ - **Turbo Integration**: Custom stream actions and frame handling
38
+
39
+ ## Detailed Implementation
40
+
41
+ ### Ruby Components
42
+
43
+ #### `UltimateTurboModal` Module (`lib/ultimate_turbo_modal.rb`)
44
+ - Entry point for the gem
45
+ - Factory method `new` creates modal instances
46
+ - `modal_class` method dynamically loads flavor classes based on configuration
47
+ - Extends self for module-level methods
48
+
49
+ #### `Base` Class (`lib/ultimate_turbo_modal/base.rb`)
50
+ - **Inheritance**: `Phlex::HTML` for HTML generation with Ruby DSL
51
+ - **Mixins**:
52
+ - `Phlex::DeferredRenderWithMainContent` for content block handling
53
+ - Dynamic inclusion of Turbo helpers (FramesHelper, StreamsHelper)
54
+ - **Key Methods**:
55
+ - `initialize`: Accepts configuration options with defaults from global config
56
+ - `view_template`: Main rendering method that wraps content in appropriate Turbo tags
57
+ - `modal`: Orchestrates HTML structure generation
58
+ - `div_*` methods: Generate specific HTML elements with proper classes and attributes
59
+ - **Data Attributes**: Passes configuration to JavaScript via data attributes on the container div
60
+
61
+ #### `Configuration` Class (`lib/ultimate_turbo_modal/configuration.rb`)
62
+ - **Options with Validation**:
63
+ - `flavor`: Symbol/String for CSS framework (default: `:tailwind`)
64
+ - `close_button`: Boolean for showing close button
65
+ - `advance`: Boolean for browser history manipulation
66
+ - `padding`: Boolean or String for content padding
67
+ - `header`, `header_divider`, `footer_divider`: Boolean display options
68
+ - `allowed_click_outside_selector`: Array of CSS selectors that won't dismiss modal
69
+ - **Type Safety**: Each setter validates input types and raises `ArgumentError` on invalid values
70
+
71
+ #### Rails Helpers
72
+
73
+ ##### `ViewHelper` (`helpers/view_helper.rb`)
74
+ - `modal` method: Renders modal component with current request context
75
+ - Instantiates `UltimateTurboModal` with passed options
76
+
77
+ ##### `ControllerHelper` (`helpers/controller_helper.rb`)
78
+ - `inside_modal?` method: Detects if request is within modal context
79
+ - Uses `Turbo-Frame` header to determine modal context
80
+ - Exposed as helper method to views
81
+
82
+ ##### `StreamHelper` (`helpers/stream_helper.rb`)
83
+ - `modal` method: Generates Turbo Stream actions for modal control
84
+ - Supports `:close` and `:hide` messages
85
+ - Creates custom `modal` stream action with message attribute
86
+
87
+ #### Flavor System
88
+ - Located in generator templates (`lib/generators/ultimate_turbo_modal/templates/flavors/`)
89
+ - Each flavor defines CSS class constants for modal elements:
90
+ - `DIV_DIALOG_CLASSES`, `DIV_OVERLAY_CLASSES`, `DIV_OUTER_CLASSES`, etc.
91
+ - Flavors inherit from `Base` and override class constants
92
+ - Supports Tailwind (v3 and v4), Vanilla CSS, and Custom implementations
93
+
94
+ ### JavaScript Components
95
+
96
+ #### Modal Controller (`javascript/modal_controller.js`)
97
+
98
+ ##### Stimulus Configuration
99
+ - **Targets**: `container`, `content`
100
+ - **Values**: `advanceUrl`, `allowedClickOutsideSelector`
101
+ - **Actions**: Responds to keyboard, click, and Turbo events
102
+
103
+ ##### Lifecycle Methods
104
+ - **`connect()`**:
105
+ - Initializes focus trap and scroll lock variables
106
+ - Shows modal immediately
107
+ - Sets up popstate listener for browser back button
108
+ - Exposes controller as `window.modal`
109
+ - **`disconnect()`**: Cleans up focus trap and global reference
110
+
111
+ ##### Core Functionality
112
+
113
+ ###### Modal Display
114
+ - **`showModal()`**:
115
+ - Locks body scroll
116
+ - Triggers enter transition
117
+ - Activates focus trap after transition
118
+ - Pushes history state if `advance` is enabled
119
+ - **`hideModal()`**:
120
+ - Prevents double-hiding with `hidingModal` flag
121
+ - Dispatches cancelable `modal:closing` event
122
+ - Deactivates focus trap
123
+ - Triggers leave transition
124
+ - Cleans up DOM and history
125
+ - Dispatches `modal:closed` event
126
+
127
+ ###### Focus Management (`#activateFocusTrap()`, `#deactivateFocusTrap()`)
128
+ - Creates focus trap with sensible defaults
129
+ - Finds first focusable element or focuses modal itself
130
+ - Handles errors gracefully without breaking modal
131
+ - Respects modal's own keyboard/click handlers
132
+
133
+ ###### Scroll Locking (`#lockBodyScroll()`, `#unlockBodyScroll()`)
134
+ - Stores current scroll position
135
+ - Sets body to `position: fixed` to prevent scroll
136
+ - Restores original overflow and scroll position on unlock
137
+ - Prevents layout shift during modal display
138
+
139
+ ###### History Management
140
+ - Uses data attribute on body to track history state
141
+ - `#hasHistoryAdvanced()`, `#setHistoryAdvanced()`, `#resetHistoryAdvanced()`
142
+ - Coordinates with browser back button via popstate listener
143
+
144
+ ###### Event Handlers
145
+ - **`submitEnd()`**: Closes modal on successful form submission
146
+ - **`closeWithKeyboard()`**: ESC key handler
147
+ - **`outsideModalClicked()`**: Dismisses modal on outside clicks unless allowed selector matches
148
+
149
+ ###### Version Checking
150
+ - `#checkVersions()`: Warns about gem/npm version mismatches in development
151
+ - Helps developers keep packages in sync
152
+
153
+ #### Main Package Entry (`javascript/index.js`)
154
+
155
+ ##### Turbo Stream Actions
156
+ - Registers custom `modal` stream action
157
+ - Handles `hide` and `close` messages via `window.modal` reference
158
+
159
+ ##### Turbo Frame Integration
160
+ - **`handleTurboFrameMissing`**: Escapes modal on redirects
161
+ - **`handleTurboBeforeFrameRender`**: Uses Idiomorph for intelligent morphing
162
+ - Prevents flicker and unwanted animations
163
+ - Morphs only innerHTML to preserve modal container
164
+
165
+ ### Modal Lifecycle Flow
166
+
167
+ 1. **Trigger**: Link/form targets `data-turbo-frame="modal"`
168
+ 2. **Request**: Rails controller renders modal content
169
+ 3. **Response**:
170
+ - If Turbo Frame request: Wrapped in `<turbo-frame id="modal">`
171
+ - If Turbo Stream: Wrapped in stream action targeting modal
172
+ 4. **Client Processing**:
173
+ - Turbo updates modal frame content
174
+ - Stimulus controller connects and shows modal
175
+ - Focus trap activates, scroll locks
176
+ - History state pushed (if enabled)
177
+ 5. **Interaction**:
178
+ - User interacts with modal content
179
+ - Form submissions handled via Turbo
180
+ - ESC key, close button, or outside clicks trigger hiding
181
+ 6. **Dismissal**:
182
+ - `modal:closing` event fired (cancelable)
183
+ - Focus trap deactivates
184
+ - Leave transition plays
185
+ - DOM cleaned up
186
+ - History restored
187
+ - `modal:closed` event fired
8
188
 
9
189
  ## Project Structure
10
190
 
@@ -12,13 +192,19 @@ Ultimate Turbo Modal (UTMR) is a full-featured modal implementation for Rails ap
12
192
  - `base.rb`: Core modal component (Phlex-based)
13
193
  - `configuration.rb`: Global configuration management
14
194
  - `helpers/`: Rails helpers for views and controllers
195
+ - `railtie.rb`: Rails integration setup
15
196
  - Generators in `/lib/generators/` for installation
16
197
 
17
198
  - **JavaScript Package**: Located in `/javascript/`
18
199
  - `modal_controller.js`: Stimulus controller for modal behavior
19
- - `index.js`: Main entry point
200
+ - `index.js`: Main entry point with Turbo integration
201
+ - `styles/`: CSS files for vanilla styling
20
202
  - Distributed files built to `/javascript/dist/`
21
203
 
204
+ - **Demo Application**: Located in `/demo-app/`
205
+ - `Procfile.dev`: Development process file for overmind/foreman
206
+ - `bin/dev`: Development script for starting the demo app
207
+
22
208
  ## Common Development Commands
23
209
 
24
210
  ### JavaScript Development (run from `/javascript/` directory)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ultimate_turbo_modal (2.1.2)
4
+ ultimate_turbo_modal (2.2.1)
5
5
  actionpack
6
6
  activesupport
7
7
  phlex-rails
data/README.md CHANGED
@@ -157,6 +157,28 @@ The demo app provides examples of:
157
157
  - Various trigger methods
158
158
  - Advanced features like scrollable content and custom footers
159
159
 
160
+ ## Updating between minor versions
161
+
162
+ To upgrade within the same major version (for example 2.1 → 2.2):
163
+
164
+ 1. Change the UTMR gem version in your `Gemfile`:
165
+
166
+ ```ruby
167
+ gem "ultimate_turbo_modal", "~> 2.2"
168
+ ```
169
+
170
+ 2. Install updated dependencies:
171
+
172
+ ```sh
173
+ bundle install
174
+ ```
175
+
176
+ 3. Run the update generator:
177
+
178
+ ```sh
179
+ bundle exec rails g ultimate_turbo_modal:update
180
+ ```
181
+
160
182
  ## Upgrading from 1.x
161
183
 
162
184
  Please see the [Upgrading Guide](UPGRADING.md) for detailed instructions on how to upgrade from version 1.x.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.1.2
1
+ 2.2.1
data/javascript/index.js CHANGED
@@ -8,10 +8,19 @@ Turbo.StreamActions.modal = function () {
8
8
  if (message == "close") window.modal?.hide();
9
9
  };
10
10
 
11
+ // Check if the event target is one of our modal Turbo Frames
12
+ const isModalFrameTarget = (event) => {
13
+ const target = event?.target;
14
+ return (
15
+ target instanceof Element &&
16
+ target.tagName.toLowerCase() === 'turbo-frame' &&
17
+ (target.id === 'modal' || target.id === 'modal-inner')
18
+ );
19
+ };
20
+
11
21
  // Escape modal from the backend on redirects
12
22
  const handleTurboFrameMissing = (event) => {
13
- if (event.detail.response.redirected &&
14
- event.target == document.querySelector("turbo-frame#modal")) {
23
+ if (event.detail.response.redirected && isModalFrameTarget(event)) {
15
24
  event.preventDefault()
16
25
  event.detail.visit(event.detail.response)
17
26
  }
@@ -19,7 +28,7 @@ const handleTurboFrameMissing = (event) => {
19
28
 
20
29
  // Morph the innerHTML of the modal to prevent flickering and transition animations
21
30
  const handleTurboBeforeFrameRender = (event) => {
22
- if (event.target.id === "modal") {
31
+ if (isModalFrameTarget(event)) {
23
32
  event.detail.render = (currentElement, newElement) => {
24
33
  Idiomorph.morph(currentElement, newElement, {
25
34
  morphstyle: 'innerHTML'
@@ -6,7 +6,7 @@ import { createFocusTrap } from 'focus-trap';
6
6
  const PACKAGE_VERSION = '__PACKAGE_VERSION__';
7
7
 
8
8
  export default class extends Controller {
9
- static targets = ["container", "content"]
9
+ static targets = ["container", "content", "overlay", "outer"]
10
10
  static values = {
11
11
  advanceUrl: String,
12
12
  allowedClickOutsideSelector: String
@@ -48,7 +48,11 @@ export default class extends Controller {
48
48
  // Lock body scroll
49
49
  this.#lockBodyScroll();
50
50
 
51
- enter(this.containerTarget).then(() => {
51
+ // Apply transitions to both overlay and outer elements
52
+ Promise.all([
53
+ enter(this.overlayTarget),
54
+ enter(this.outerTarget)
55
+ ]).then(() => {
52
56
  // Activate focus trap after the modal transition is complete
53
57
  this.#activateFocusTrap();
54
58
  });
@@ -122,7 +126,11 @@ export default class extends Controller {
122
126
  // Unlock body scroll
123
127
  this.#unlockBodyScroll();
124
128
 
125
- leave(this.containerTarget).then(() => {
129
+ // Apply leave transitions to both overlay and outer elements
130
+ Promise.all([
131
+ leave(this.overlayTarget),
132
+ leave(this.outerTarget)
133
+ ]).then(() => {
126
134
  this.turboFrame.removeAttribute("src");
127
135
  this.containerTarget.remove();
128
136
  this.#resetHistoryAdvanced();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate_turbo_modal",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "The ultimate Turbo / Stimulus / Hotwire modal window for Rails",
5
5
  "main": "dist/ultimate_turbo_modal.min.js",
6
6
  "module": "dist/ultimate_turbo_modal.min.js",
@@ -10,6 +10,7 @@
10
10
  "scripts": {
11
11
  "update-version": "node scripts/update-version.js",
12
12
  "build": "yarn install && rollup -c",
13
+ "build:watch": "rollup -c -w",
13
14
  "release": "bash scripts/release-npm.sh"
14
15
  },
15
16
  "repository": {
@@ -1,3 +1,7 @@
1
+ .hidden {
2
+ display: none
3
+ }
4
+
1
5
  .dark {
2
6
  .modal-overlay {
3
7
  background-color: rgba(17, 24, 39, 0.8)
@@ -177,3 +181,85 @@
177
181
  width: 1.25rem;
178
182
  height: 1.25rem;
179
183
  }
184
+
185
+ /*
186
+ Transition utilities for Vanilla flavor
187
+ These replicate the Tailwind transitions defined in tailwind.rb
188
+ using plain CSS classes referenced in vanilla.rb
189
+ */
190
+
191
+ /* Overlay: Enter */
192
+ .modal-transition-overlay-enter-animation {
193
+ transition-property: opacity;
194
+ transition-duration: 300ms; /* duration-300 */
195
+ transition-timing-function: ease-out; /* ease-out */
196
+ }
197
+
198
+ .modal-transition-overlay-enter-start {
199
+ opacity: 0; /* opacity-0 */
200
+ }
201
+
202
+ .modal-transition-overlay-enter-end {
203
+ opacity: 1; /* opacity-100 */
204
+ }
205
+
206
+ /* Overlay: Leave */
207
+ .modal-transition-overlay-leave-animation {
208
+ transition-property: opacity;
209
+ transition-duration: 200ms; /* duration-200 */
210
+ transition-timing-function: ease-in; /* ease-in */
211
+ }
212
+
213
+ .modal-transition-overlay-leave-start {
214
+ opacity: 1; /* opacity-100 */
215
+ }
216
+
217
+ .modal-transition-overlay-leave-end {
218
+ opacity: 0; /* opacity-0 */
219
+ }
220
+
221
+ /* Dialog: Enter */
222
+ .modal-transition-dialog-enter-animation {
223
+ transition-property: opacity, transform;
224
+ transition-duration: 300ms; /* duration-300 */
225
+ transition-timing-function: ease-out; /* ease-out */
226
+ }
227
+
228
+ .modal-transition-dialog-enter-start {
229
+ opacity: 0; /* opacity-0 */
230
+ transform: translateY(1rem) scale(1); /* translate-y-4 */
231
+ }
232
+
233
+ @media (min-width: 640px) {
234
+ .modal-transition-dialog-enter-start {
235
+ transform: translateY(0) scale(0.95); /* sm:translate-y-0 sm:scale-95 */
236
+ }
237
+ }
238
+
239
+ .modal-transition-dialog-enter-end {
240
+ opacity: 1; /* opacity-100 */
241
+ transform: translateY(0) scale(1); /* translate-y-0 scale-100 */
242
+ }
243
+
244
+ /* Dialog: Leave */
245
+ .modal-transition-dialog-leave-animation {
246
+ transition-property: opacity, transform;
247
+ transition-duration: 200ms; /* duration-200 */
248
+ transition-timing-function: ease-in; /* ease-in */
249
+ }
250
+
251
+ .modal-transition-dialog-leave-start {
252
+ opacity: 1; /* opacity-100 */
253
+ transform: translateY(0) scale(1); /* translate-y-0 scale-100 */
254
+ }
255
+
256
+ .modal-transition-dialog-leave-end {
257
+ opacity: 0; /* opacity-0 */
258
+ transform: translateY(1rem) scale(1); /* translate-y-4 */
259
+ }
260
+
261
+ @media (min-width: 640px) {
262
+ .modal-transition-dialog-leave-end {
263
+ transform: translateY(0) scale(0.95); /* sm:translate-y-0 sm:scale-95 */
264
+ }
265
+ }
@@ -4,9 +4,9 @@
4
4
  # TODO: define the classes for each HTML element.
5
5
  module UltimateTurboModal::Flavors
6
6
  class Custom < UltimateTurboModal::Base
7
- DIV_DIALOG_CLASSES = ""
7
+ DIV_MODAL_CONTAINER_CLASSES = ""
8
8
  DIV_OVERLAY_CLASSES = ""
9
- DIV_OUTER_CLASSES = ""
9
+ DIV_DIALOG_CLASSES = ""
10
10
  DIV_INNER_CLASSES = ""
11
11
  DIV_CONTENT_CLASSES = ""
12
12
  DIV_MAIN_CLASSES = ""
@@ -18,5 +18,32 @@ module UltimateTurboModal::Flavors
18
18
  BUTTON_CLOSE_SR_ONLY_CLASSES = ""
19
19
  CLOSE_BUTTON_TAG_CLASSES = ""
20
20
  ICON_CLOSE_CLASSES = ""
21
+
22
+ TRANSITIONS = {
23
+ overlay: {
24
+ enter: {
25
+ animation: "",
26
+ start: "",
27
+ end: ""
28
+ },
29
+ leave: {
30
+ animation: "",
31
+ start: "",
32
+ end: ""
33
+ }
34
+ },
35
+ dialog: {
36
+ enter: {
37
+ animation: "",
38
+ start: "",
39
+ end: ""
40
+ },
41
+ leave: {
42
+ animation: "",
43
+ start: "",
44
+ end: ""
45
+ }
46
+ }
47
+ }
21
48
  end
22
49
  end
@@ -3,9 +3,9 @@
3
3
  # Tailwind CSS v4
4
4
  module UltimateTurboModal::Flavors
5
5
  class Tailwind < UltimateTurboModal::Base
6
- DIV_DIALOG_CLASSES = "relative group z-50"
7
- DIV_OVERLAY_CLASSES = "fixed inset-0 bg-gray-900/70 transition-opacity dark:bg-gray-900/80"
8
- DIV_OUTER_CLASSES = "fixed inset-0 overflow-y-auto sm:max-w-[80%] md:max-w-3xl sm:mx-auto m-4"
6
+ DIV_MODAL_CONTAINER_CLASSES = "relative group z-50"
7
+ DIV_OVERLAY_CLASSES = "fixed inset-0 bg-gray-900/70 transition-opacity dark:bg-gray-900/80 opacity-0"
8
+ DIV_DIALOG_CLASSES = "fixed inset-0 overflow-y-auto sm:max-w-[80%] md:max-w-3xl sm:mx-auto m-4 opacity-0"
9
9
  DIV_INNER_CLASSES = "flex min-h-full items-start justify-center pt-[10vh] sm:p-4"
10
10
  DIV_CONTENT_CLASSES = "relative transform max-h-screen overflow-hidden rounded-lg bg-white text-left shadow-lg transition-all sm:my-8 sm:max-w-3xl dark:bg-gray-800 dark:text-white"
11
11
  DIV_MAIN_CLASSES = "group-data-[padding=true]:p-4 group-data-[padding=true]:pt-2 overflow-y-auto max-h-[75vh]"
@@ -17,5 +17,32 @@ module UltimateTurboModal::Flavors
17
17
  BUTTON_CLOSE_SR_ONLY_CLASSES = "sr-only"
18
18
  CLOSE_BUTTON_TAG_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"
19
19
  ICON_CLOSE_CLASSES = "w-5 h-5"
20
+
21
+ TRANSITIONS = {
22
+ overlay: {
23
+ enter: {
24
+ animation: "ease-out duration-300",
25
+ start: "opacity-0",
26
+ end: "opacity-100"
27
+ },
28
+ leave: {
29
+ animation: "ease-in duration-200",
30
+ start: "opacity-100",
31
+ end: "opacity-0"
32
+ }
33
+ },
34
+ dialog: {
35
+ enter: {
36
+ animation: "ease-out duration-300",
37
+ start: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
38
+ end: "opacity-100 translate-y-0 sm:scale-100"
39
+ },
40
+ leave: {
41
+ animation: "ease-in duration-200",
42
+ start: "opacity-100 translate-y-0 sm:scale-100",
43
+ end: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
44
+ }
45
+ }
46
+ }
20
47
  end
21
48
  end
@@ -3,9 +3,9 @@
3
3
  # Tailwind CSS v3
4
4
  module UltimateTurboModal::Flavors
5
5
  class Tailwind3 < UltimateTurboModal::Base
6
- DIV_DIALOG_CLASSES = "relative z-50"
6
+ DIV_MODAL_CONTAINER_CLASSES = "relative z-50"
7
7
  DIV_OVERLAY_CLASSES = "fixed inset-0 bg-gray-900 bg-opacity-70 transition-opacity dark:bg-gray-900 dark:bg-opacity-80"
8
- DIV_OUTER_CLASSES = "fixed inset-0 overflow-y-auto sm:max-w-[80%] md:max-w-3xl sm:mx-auto m-4"
8
+ DIV_DIALOG_CLASSES = "fixed inset-0 overflow-y-auto sm:max-w-[80%] md:max-w-3xl sm:mx-auto m-4"
9
9
  DIV_INNER_CLASSES = "flex min-h-full items-start justify-center pt-[10vh] sm:p-4"
10
10
  DIV_CONTENT_CLASSES = "relative transform max-h-screen overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:max-w-3xl dark:bg-gray-800 dark:text-white"
11
11
  DIV_MAIN_CLASSES = "p-4 pt-2 overflow-y-auto max-h-[75vh]"
@@ -17,5 +17,32 @@ module UltimateTurboModal::Flavors
17
17
  BUTTON_CLOSE_SR_ONLY_CLASSES = "sr-only"
18
18
  CLOSE_BUTTON_TAG_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"
19
19
  ICON_CLOSE_CLASSES = "w-5 h-5"
20
+
21
+ TRANSITIONS = {
22
+ overlay: {
23
+ enter: {
24
+ animation: "ease-out duration-300",
25
+ start: "opacity-0",
26
+ end: "opacity-100"
27
+ },
28
+ leave: {
29
+ animation: "ease-in duration-200",
30
+ start: "opacity-100",
31
+ end: "opacity-0"
32
+ }
33
+ },
34
+ dialog: {
35
+ enter: {
36
+ animation: "ease-out duration-300",
37
+ start: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
38
+ end: "opacity-100 translate-y-0 sm:scale-100"
39
+ },
40
+ leave: {
41
+ animation: "ease-in duration-200",
42
+ start: "opacity-100 translate-y-0 sm:scale-100",
43
+ end: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
44
+ }
45
+ }
46
+ }
20
47
  end
21
48
  end
@@ -3,9 +3,10 @@
3
3
  # Vanilla CSS
4
4
  module UltimateTurboModal::Flavors
5
5
  class Vanilla < UltimateTurboModal::Base
6
- DIV_DIALOG_CLASSES = "modal-container"
7
- DIV_OVERLAY_CLASSES = "modal-overlay"
8
- DIV_OUTER_CLASSES = "modal-outer"
6
+ DIV_MODAL_CONTAINER_CLASSES = "modal-container"
7
+ # Include enter-start classes so initial paint is hidden and transitions can animate smoothly
8
+ DIV_OVERLAY_CLASSES = "modal-overlay modal-transition-overlay-enter-start"
9
+ DIV_DIALOG_CLASSES = "modal-outer modal-transition-dialog-enter-start"
9
10
  DIV_INNER_CLASSES = "modal-inner"
10
11
  DIV_CONTENT_CLASSES = "modal-content"
11
12
  DIV_MAIN_CLASSES = "modal-main"
@@ -17,5 +18,32 @@ module UltimateTurboModal::Flavors
17
18
  BUTTON_CLOSE_SR_ONLY_CLASSES = "sr-only"
18
19
  CLOSE_BUTTON_TAG_CLASSES = "modal-close-button"
19
20
  ICON_CLOSE_CLASSES = "modal-close-icon"
21
+
22
+ TRANSITIONS = {
23
+ overlay: {
24
+ enter: {
25
+ animation: "modal-transition-overlay-enter-animation",
26
+ start: "modal-transition-overlay-enter-start",
27
+ end: "modal-transition-overlay-enter-end"
28
+ },
29
+ leave: {
30
+ animation: "modal-transition-overlay-leave-animation",
31
+ start: "modal-transition-overlay-leave-start",
32
+ end: "modal-transition-overlay-leave-end"
33
+ }
34
+ },
35
+ dialog: {
36
+ enter: {
37
+ animation: "modal-transition-dialog-enter-animation",
38
+ start: "modal-transition-dialog-enter-start",
39
+ end: "modal-transition-dialog-enter-end"
40
+ },
41
+ leave: {
42
+ animation: "modal-transition-dialog-leave-animation",
43
+ start: "modal-transition-dialog-leave-start",
44
+ end: "modal-transition-dialog-leave-end"
45
+ }
46
+ }
47
+ }
20
48
  end
21
49
  end