ultimate_turbo_modal 2.1.1 → 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 +15 -0
  4. data/CLAUDE.md +190 -4
  5. data/Gemfile.lock +1 -1
  6. data/README.md +55 -2
  7. data/VERSION +1 -1
  8. data/javascript/index.js +12 -3
  9. data/javascript/modal_controller.js +52 -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 +35 -14
  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 -7
  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 -1022
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '070742966b297c5aec10941f765cba344f5f4a5c4157dfa81f0fbafabcd3b329'
4
- data.tar.gz: 2ad6633a34d074276d774b6665ae581561a1b06b96dac8e59660dddc9ded2674
3
+ metadata.gz: f48f99a3169d751eb02f9efe0f357aa70b285097e03438e2d1b27d1693b9b59c
4
+ data.tar.gz: c333e8c524f3c2967d1ddc484aab887165f574e0969bdd1b1aa6d1a48752b7ba
5
5
  SHA512:
6
- metadata.gz: 23e754a2b8b76194129380d520303652025eb763577b592ff0daf95eb0e2800883fccf0772f6ff3159ac03941655b4860c3327ce37a16bdc0a77749d2d56bb2e
7
- data.tar.gz: 9d7f91f17a90ec51fb75c84d9ce11fc31a98b02b92bf4aaa7e3221c9496f8c390d198566af23d81996bc28229fb71890791e7aad55a42f3ab3fedeb25c3c184a
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,18 @@
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
+
12
+ ## [2.1.2] - 2025-08-06
13
+
14
+ - Fixed scroll lock
15
+
1
16
  ## [2.1.1] - 2025-08-05
2
17
 
3
18
  - Reduce Rails dependency to only required components (actionpack, activesupport, railties) (#22)
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)
@@ -79,4 +265,4 @@ When adding a new option:
79
265
  ## Testing Approach
80
266
  - JavaScript: No test framework currently set up
81
267
  - Ruby: Use standard Rails testing practices
82
- - Manual testing via the demo app: https://github.com/cmer/ultimate_turbo_modal-demo
268
+ - Manual testing via the demo app (located in `./demo-app`)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ultimate_turbo_modal (2.1.1)
4
+ ultimate_turbo_modal (2.2.1)
5
5
  actionpack
6
6
  activesupport
7
7
  phlex-rails
data/README.md CHANGED
@@ -122,9 +122,62 @@ Do not get overwhelmed with all the options. The defaults are sensible.
122
122
  - Focus trap for improved accessibility (Tab and Shift+Tab cycle through focusable elements within the modal only)
123
123
 
124
124
 
125
- ## Demo
125
+ ## Demo Video
126
126
 
127
- A demo application can be found at https://github.com/cmer/ultimate_turbo_modal-demo. A video demo can be seen here: [https://youtu.be/BVRDXLN1I78](https://youtu.be/BVRDXLN1I78).
127
+ A video demo can be seen here: [https://youtu.be/BVRDXLN1I78](https://youtu.be/BVRDXLN1I78).
128
+
129
+ ### Running the Demo Application
130
+
131
+ The repository includes a demo application in the `demo-app` directory that showcases all the features of Ultimate Turbo Modal. To run it locally:
132
+
133
+ ```bash
134
+ # Navigate to the demo app directory
135
+ cd demo-app
136
+
137
+ # Install Ruby dependencies
138
+ bundle install
139
+
140
+ # Create and setup the database
141
+ bin/rails db:create db:migrate db:seed
142
+
143
+ # Install JavaScript dependencies
144
+ yarn install
145
+
146
+ # Start the development server
147
+ bin/dev
148
+
149
+ # Open your browser
150
+ open http://localhost:3000
151
+ ```
152
+
153
+ The demo app provides examples of:
154
+ - Basic modal usage
155
+ - Different modal configurations
156
+ - Custom styling options
157
+ - Various trigger methods
158
+ - Advanced features like scrollable content and custom footers
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
+ ```
128
181
 
129
182
  ## Upgrading from 1.x
130
183
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.1.1
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
@@ -20,6 +20,10 @@ export default class extends Controller {
20
20
  // Initialize focus trap instance variable
21
21
  this.focusTrapInstance = null;
22
22
 
23
+ // Store original body styles for scroll lock
24
+ this.originalBodyOverflow = null;
25
+ this.scrollPosition = 0;
26
+
23
27
  this.showModal();
24
28
 
25
29
  this.turboFrame = this.element.closest('turbo-frame');
@@ -41,7 +45,14 @@ export default class extends Controller {
41
45
  }
42
46
 
43
47
  showModal() {
44
- enter(this.containerTarget).then(() => {
48
+ // Lock body scroll
49
+ this.#lockBodyScroll();
50
+
51
+ // Apply transitions to both overlay and outer elements
52
+ Promise.all([
53
+ enter(this.overlayTarget),
54
+ enter(this.outerTarget)
55
+ ]).then(() => {
45
56
  // Activate focus trap after the modal transition is complete
46
57
  this.#activateFocusTrap();
47
58
  });
@@ -112,7 +123,14 @@ export default class extends Controller {
112
123
  }
113
124
 
114
125
  #resetModalElement() {
115
- leave(this.containerTarget).then(() => {
126
+ // Unlock body scroll
127
+ this.#unlockBodyScroll();
128
+
129
+ // Apply leave transitions to both overlay and outer elements
130
+ Promise.all([
131
+ leave(this.overlayTarget),
132
+ leave(this.outerTarget)
133
+ ]).then(() => {
116
134
  this.turboFrame.removeAttribute("src");
117
135
  this.containerTarget.remove();
118
136
  this.#resetHistoryAdvanced();
@@ -187,4 +205,35 @@ export default class extends Controller {
187
205
  this.focusTrapInstance = null;
188
206
  }
189
207
  }
208
+
209
+ #lockBodyScroll() {
210
+ // Store the current scroll position
211
+ this.scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
212
+
213
+ // Store the original overflow style
214
+ this.originalBodyOverflow = document.body.style.overflow;
215
+
216
+ // Prevent scrolling on the body
217
+ document.body.style.overflow = 'hidden';
218
+ document.body.style.position = 'fixed';
219
+ document.body.style.top = `-${this.scrollPosition}px`;
220
+ document.body.style.width = '100%';
221
+ }
222
+
223
+ #unlockBodyScroll() {
224
+ // Restore the original overflow style
225
+ if (this.originalBodyOverflow !== null) {
226
+ document.body.style.overflow = this.originalBodyOverflow;
227
+ } else {
228
+ document.body.style.removeProperty('overflow');
229
+ }
230
+
231
+ // Remove position styles
232
+ document.body.style.removeProperty('position');
233
+ document.body.style.removeProperty('top');
234
+ document.body.style.removeProperty('width');
235
+
236
+ // Restore the scroll position
237
+ window.scrollTo(0, this.scrollPosition);
238
+ }
190
239
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate_turbo_modal",
3
- "version": "2.1.0",
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