pop_frame 0.1.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 +7 -0
- data/CHANGELOG.md +10 -0
- data/MIT-LICENSE +20 -0
- data/README.md +169 -0
- data/app/assets/stylesheets/pop_frame.css +7 -0
- data/lib/pop_frame/engine.rb +12 -0
- data/lib/pop_frame/helper.rb +16 -0
- data/lib/pop_frame/version.rb +3 -0
- data/lib/pop_frame.rb +3 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c97cbc621cda0a6f45a67e3eefd506adc5a5f3afa77d8f772b046e47a9eeb6f5
|
|
4
|
+
data.tar.gz: f897c73e4d3d9dc7a56bbe31456d6c3ef9794fbd9908ad2fb1c011f809b04419
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3cc96e33d4d42113337b7e158f67a6069c076bcd382f780bed30be103457e26709ad636baa80c73b67dd4856138c17a7a826dd0e87d3ea37a6231f7b2746d245
|
|
7
|
+
data.tar.gz: 16a5bdd0e5e4c0f8c470bda16dbfc10f1c3e4f7a33a51d3337ffd1adaf3dd33152f9a108b70c407d7387e5f44b90444baaed7d3b6cd1b607517d9d31f3e998c5
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
## [v0.1.0] Oct 9, 2026
|
|
4
|
+
|
|
5
|
+
- `pop_frame_tag(id, src) { trigger }` renders a button that opens a native popover holding a Turbo
|
|
6
|
+
Frame. The frame loads `src` (anything `url_for` takes) the first time the popover opens, and
|
|
7
|
+
reloads in place on a morph refresh, staying open. `pop_frame.css` anchors the popover to its
|
|
8
|
+
button.
|
|
9
|
+
|
|
10
|
+
[v0.1.0]: https://github.com/RoleModel/pop-frame/releases/tag/v0.1.0
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2026 RoleModel Software
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Pop Frame
|
|
2
|
+
|
|
3
|
+
<div>
|
|
4
|
+
<img src="https://github.com/RoleModel/pop-frame/actions/workflows/ci.yml/badge.svg" alt="CI">
|
|
5
|
+
<img src="https://img.shields.io/gem/v/pop_frame" alt="Gem Version">
|
|
6
|
+
</div>
|
|
7
|
+
<br/>
|
|
8
|
+
|
|
9
|
+
A `turbo-frame` in a native popover, loaded the first time it opens.
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
Popover libraries were built for browsers that couldn't open a popover, position it, or keep it
|
|
14
|
+
from being clipped. Browsers can do all of that now. A JavaScript popover also has to be re-wired
|
|
15
|
+
every time Turbo morphs the page; a native one is just HTML, so a morph leaves it alone.
|
|
16
|
+
|
|
17
|
+
Pop Frame is one view helper and five lines of CSS. The browser handles the rest:
|
|
18
|
+
|
|
19
|
+
- The [Popover API][mdn-popover] opens and closes it, closes it on Escape or an outside click, and
|
|
20
|
+
keeps `aria-expanded` up to date.
|
|
21
|
+
- The [top layer][mdn-top-layer] keeps it above everything, so an `overflow` ancestor can't clip it.
|
|
22
|
+
- [CSS anchor positioning][mdn-anchor] places it under the button that opened it.
|
|
23
|
+
- Turbo loads the frame and refreshes it when the page morphs.
|
|
24
|
+
|
|
25
|
+
There's no JavaScript to install, configure, or keep alive across morphs.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bundle add pop_frame
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then include the stylesheet. With Propshaft or Sprockets, add it to your layout:
|
|
34
|
+
|
|
35
|
+
```erb
|
|
36
|
+
<%= stylesheet_link_tag 'pop_frame' %>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
If a JavaScript bundler builds your CSS (cssbundling-rails, webpack), copy
|
|
40
|
+
[`pop_frame.css`](app/assets/stylesheets/pop_frame.css) into your own stylesheets instead.
|
|
41
|
+
|
|
42
|
+
## Basic Usage
|
|
43
|
+
|
|
44
|
+
Give `pop_frame_tag` an id for the frame and where to load it from. The block becomes the button's
|
|
45
|
+
content. (Examples use [slim] syntax.)
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
= pop_frame_tag dom_id(person, :week), person_week_path(person), class: 'btn' do
|
|
49
|
+
| This week
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
which renders
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<button type="button" popovertarget="person_1_week_popover" class="btn">This week</button>
|
|
56
|
+
<div id="person_1_week_popover" class="pop-frame" popover="auto">
|
|
57
|
+
<turbo-frame id="person_1_week" src="/people/1/week" loading="lazy" refresh="morph">Loading…</turbo-frame>
|
|
58
|
+
</div>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The response must contain a `turbo-frame` with the same id, as with any Turbo Frame:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
= turbo_frame_tag dom_id(@person, :week) do
|
|
65
|
+
= render 'people/week', person: @person
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Nothing is fetched until the popover first opens. After that, it keeps its content between opens.
|
|
69
|
+
Links and forms inside it navigate the frame, just as they would in any other `turbo-frame`.
|
|
70
|
+
|
|
71
|
+
> [!TIP]
|
|
72
|
+
> Each frame needs an id that's unique on the page. `dom_id` with a suffix, as above, is an easy way
|
|
73
|
+
> to get one, even when the same record shows up in several places.
|
|
74
|
+
|
|
75
|
+
## Options
|
|
76
|
+
|
|
77
|
+
`pop_frame_tag(id, src, loading: 'Loading…', **button_options, &block)`
|
|
78
|
+
|
|
79
|
+
| Argument | Description | Default |
|
|
80
|
+
| ---------------- | ----------------------------------------------------------------------------------- | ------------ |
|
|
81
|
+
| `id` | The frame's id. The popover's id is derived from it (`"#{id}_popover"`). | required |
|
|
82
|
+
| `src` | Where the frame loads from. Takes anything [`url_for`][url-for] does. | required |
|
|
83
|
+
| `loading:` | Shown until the frame loads: plain text, or markup like `render 'spinner'`. | `'Loading…'` |
|
|
84
|
+
| `button_options` | HTML options for the button (`class:`, `data:`, `aria:` and so on). | `{}` |
|
|
85
|
+
| `&block` | The button's content. | required |
|
|
86
|
+
|
|
87
|
+
## Styling
|
|
88
|
+
|
|
89
|
+
The stylesheet only positions the popover. Its look is up to you:
|
|
90
|
+
|
|
91
|
+
```css
|
|
92
|
+
/* the popover */
|
|
93
|
+
.pop-frame { padding: 1rem; border-radius: 8px; box-shadow: 0 8px 24px #0002; }
|
|
94
|
+
|
|
95
|
+
/* its button, while open */
|
|
96
|
+
.btn:has(+ :popover-open) { outline: 2px solid royalblue; }
|
|
97
|
+
|
|
98
|
+
/* the frame, while it loads */
|
|
99
|
+
.pop-frame turbo-frame[busy] { opacity: 0.5; }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The popover opens below its button, aligned to the button's start edge, and flips to fit when it's
|
|
103
|
+
near the edge of the screen. To place it differently, override `position-area` and
|
|
104
|
+
`position-try-fallbacks` on `.pop-frame[popover]`.
|
|
105
|
+
|
|
106
|
+
## Page Refreshes
|
|
107
|
+
|
|
108
|
+
Pop Frame is built for pages that [refresh with morphing][turbo-morph]. When a save elsewhere on the
|
|
109
|
+
page (or a broadcast) morphs it:
|
|
110
|
+
|
|
111
|
+
- An open popover **stays open**, and its content is swapped for the fresh version as soon as it
|
|
112
|
+
arrives, with no flash of the loading state.
|
|
113
|
+
- Popovers that were opened earlier and are now closed reload in the background, so they're current
|
|
114
|
+
the next time they open.
|
|
115
|
+
- Popovers that were never opened stay unloaded.
|
|
116
|
+
|
|
117
|
+
> [!NOTE]
|
|
118
|
+
> Morph refreshes need `turbo_refreshes_with method: :morph` in the page's `<head>`. That helper
|
|
119
|
+
> writes to `content_for(:head)`, so your layout has to `yield :head`. If it doesn't, use
|
|
120
|
+
> `turbo_refresh_method_tag :morph` in the layout instead.
|
|
121
|
+
|
|
122
|
+
## Browser Support
|
|
123
|
+
|
|
124
|
+
The popover itself works anywhere the Popover API does (Baseline 2024). Positioning it under its
|
|
125
|
+
button needs CSS anchor positioning: Chrome 125, Safari 26, Firefox 147. Older browsers still open
|
|
126
|
+
the popover, just centered on the screen by the browser's default popover styles.
|
|
127
|
+
|
|
128
|
+
Opening a popover from your own JavaScript with `showPopover()` leaves it with nothing to anchor to.
|
|
129
|
+
Pass the button as its source, `popover.showPopover({ source: button })`, to keep it in place.
|
|
130
|
+
|
|
131
|
+
## System Tests
|
|
132
|
+
|
|
133
|
+
Because it's ordinary HTML, Capybara needs nothing special:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
click_on 'This week'
|
|
137
|
+
|
|
138
|
+
within '.pop-frame' do
|
|
139
|
+
assert_text 'Week of Oct 12'
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Development
|
|
144
|
+
|
|
145
|
+
After cloning the repository, install dependencies with `bundle install`.
|
|
146
|
+
|
|
147
|
+
Run the test suite with `bin/rails test:all`. Its system tests drive headless Chrome with [Cuprite].
|
|
148
|
+
Lint with `bin/rubocop`.
|
|
149
|
+
|
|
150
|
+
Run the dummy app's server on port 3000 with `bin/dev` (set `PORT` to change it). It's a page of
|
|
151
|
+
popovers to try by hand.
|
|
152
|
+
|
|
153
|
+
Releases are cut from the **Bump Version** workflow in the Actions tab. It takes the version bump and
|
|
154
|
+
a changelog entry, then tags the release, publishes it on GitHub, and pushes the gem to RubyGems.
|
|
155
|
+
|
|
156
|
+
## Acknowledgments
|
|
157
|
+
|
|
158
|
+
**Pop Frame** is [MIT-licensed](MIT-LICENSE), open-source software from [RoleModel Software][rms].
|
|
159
|
+
|
|
160
|
+
[RoleModel Software][rms] is a world-class, collaborative software development team dedicated to delivering the highest quality custom web and mobile software solutions while cultivating a work environment where community, family, learning, and mentoring flourish.
|
|
161
|
+
|
|
162
|
+
[slim]: https://github.com/slim-template/slim
|
|
163
|
+
[mdn-popover]: https://developer.mozilla.org/en-US/docs/Web/API/Popover_API
|
|
164
|
+
[mdn-top-layer]: https://developer.mozilla.org/en-US/docs/Glossary/Top_layer
|
|
165
|
+
[mdn-anchor]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning
|
|
166
|
+
[url-for]: https://api.rubyonrails.org/classes/ActionView/RoutingUrlFor.html#method-i-url_for
|
|
167
|
+
[turbo-morph]: https://turbo.hotwired.dev/handbook/page_refreshes
|
|
168
|
+
[Cuprite]: https://github.com/rubycdp/cuprite
|
|
169
|
+
[rms]: https://rolemodelsoftware.com/
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module PopFrame
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
initializer 'pop_frame.helper' do
|
|
4
|
+
ActiveSupport.on_load(:action_view) { include PopFrame::Helper }
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
# Propshaft puts every engine's app/assets on the load path by itself; Sprockets wants it named.
|
|
8
|
+
initializer 'pop_frame.assets' do |app|
|
|
9
|
+
app.config.assets.precompile << 'pop_frame.css' if app.config.respond_to?(:assets)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module PopFrame
|
|
2
|
+
module Helper
|
|
3
|
+
# A button whose block is its content, opening a popover with a Turbo Frame that loads +src+
|
|
4
|
+
# (anything url_for takes) the first time it's shown, and reloads in place on a morph refresh.
|
|
5
|
+
def pop_frame_tag(id, src, loading: 'Loading…', **button_options, &)
|
|
6
|
+
popover_id = "#{id}_popover"
|
|
7
|
+
|
|
8
|
+
safe_join [
|
|
9
|
+
tag.button(type: 'button', popovertarget: popover_id, **button_options, &),
|
|
10
|
+
tag.div(id: popover_id, class: 'pop-frame', popover: 'auto') do
|
|
11
|
+
tag.turbo_frame(loading, id:, src: url_for(src), loading: 'lazy', refresh: 'morph')
|
|
12
|
+
end
|
|
13
|
+
]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/pop_frame.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pop_frame
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andy Cohen
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: actionview
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 7.1.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 7.1.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: railties
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 7.1.0
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 7.1.0
|
|
40
|
+
description: A view helper for a button that opens a lazily loaded Turbo Frame in
|
|
41
|
+
a native popover, anchored to the button with CSS. No JavaScript of its own.
|
|
42
|
+
email:
|
|
43
|
+
- andy.cohen@rolemodelsoftware.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- CHANGELOG.md
|
|
49
|
+
- MIT-LICENSE
|
|
50
|
+
- README.md
|
|
51
|
+
- app/assets/stylesheets/pop_frame.css
|
|
52
|
+
- lib/pop_frame.rb
|
|
53
|
+
- lib/pop_frame/engine.rb
|
|
54
|
+
- lib/pop_frame/helper.rb
|
|
55
|
+
- lib/pop_frame/version.rb
|
|
56
|
+
homepage: https://github.com/RoleModel/pop-frame
|
|
57
|
+
licenses:
|
|
58
|
+
- MIT
|
|
59
|
+
metadata:
|
|
60
|
+
homepage_uri: https://github.com/RoleModel/pop-frame
|
|
61
|
+
source_code_uri: https://github.com/RoleModel/pop-frame
|
|
62
|
+
changelog_uri: https://github.com/RoleModel/pop-frame/blob/main/CHANGELOG.md
|
|
63
|
+
rdoc_options: []
|
|
64
|
+
require_paths:
|
|
65
|
+
- lib
|
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '3.1'
|
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
requirements: []
|
|
77
|
+
rubygems_version: 4.0.16
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: A Turbo Frame in a native popover, loaded when it opens.
|
|
80
|
+
test_files: []
|