sdr_view_components 0.12.1 → 0.13.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: 27d09062e51eb9ab9697ca9caa7d192eb21556ed459a772dc91d87c36dca16f7
4
- data.tar.gz: 82116285f4e62913f9e9ae85e95e0272a3b1fdd928142c511a9de32721bacd90
3
+ metadata.gz: a345126e4b1ae936afb830c142944b842691edbf460fd2092136905f4035a1b2
4
+ data.tar.gz: 4dcce16061ff42a6d46fc74fa91b432b60298fb96aa22860ed752d43c09aefa1
5
5
  SHA512:
6
- metadata.gz: 0ae59ed8026d5b6fade7ae12f2ea8cd22e807dd0a849c6cf776358b3f6c6567ef93ee8fa12af7f8d8f387fdbac3a1b848dce0583fb72f1e6f3629cbfce58662e
7
- data.tar.gz: 04e8b8442faea9693c4fb34e7496477c1a694739852438ee85fe24cbc5f5120cc492cdfdedb7b23076cdd82f6cd0d6549ca6137f78c13b4318a31ff8569f3174
6
+ metadata.gz: d1e085e557f9a045c2915b8f944db1ebac286fd6245e6e22b80202bf1ee87cbe1efc2e74f0c2f9650959c9ea40a35c63049d6085a67e9a9485fc623b1bad90e3
7
+ data.tar.gz: 1ebaa46555633944bbe4a301193fde11929668392dad3457559991ca0d279774f0a0e56b0ce3dda08f34690db49d6cd13054e8a057fc8e3796a1fbb40725382b
data/README.md CHANGED
@@ -29,6 +29,11 @@ with the most recent date tagged release to your `application.html.erb` layout f
29
29
 
30
30
  Some components require JavaScript. The gem ships Stimulus controllers under `app/javascript/sdr_view_components/` and registers that path with the asset pipeline automatically.
31
31
 
32
+ The controllers that use Bootstrap's JavaScript import it as an ES module (`import * as bootstrap from "bootstrap"`), so `bootstrap` must be pinned to an ES module build of Bootstrap. The UMD `bootstrap.bundle.min.js` has no ES module exports and won't work. For example, in `config/importmap.rb`:
33
+ ```ruby
34
+ pin "bootstrap", to: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/+esm"
35
+ ```
36
+
32
37
  Here is an example of how to add a Stimulus controller:
33
38
 
34
39
  The disappearing toast uses `sdr_view_components/toast_controller` to remove itself from the DOM after its fade-out animation completes.
@@ -78,6 +83,19 @@ import TabSelectController from "sdr_view_components/tab_select_controller"
78
83
  application.register("sdr-tab-select", TabSelectController)
79
84
  ```
80
85
 
86
+ Its `overflow_menu` option similarly ships `sdr_view_components/tab_overflow_controller`, registered as `sdr-tab-overflow`:
87
+
88
+ ```ruby
89
+ pin "sdr_view_components/tab_overflow_controller", to: "sdr_view_components/tab_overflow_controller.js"
90
+ ```
91
+
92
+ ```javascript
93
+ import { application } from "controllers/application"
94
+ import TabOverflowController from "sdr_view_components/tab_overflow_controller"
95
+
96
+ application.register("sdr-tab-overflow", TabOverflowController)
97
+ ```
98
+
81
99
  ## Usage
82
100
 
83
101
  ### Form components
@@ -185,6 +203,8 @@ See the Lookbook preview for `SdrViewComponents::TabForm::TabListComponent` for
185
203
  <% end %>
186
204
  ```
187
205
 
206
+ `TabListComponent` also accepts `overflow_menu: true`. When given, tabs that don't fit on a single row are moved into a trailing "More" dropdown tab instead of wrapping, and moved back out as space allows. It can be combined with `collapse_below:`. This requires the `sdr-tab-overflow` controller -- see [JavaScript](#javascript) above.
207
+
188
208
  ```
189
209
  <% render SdrViewComponent::....>
190
210
  ```
@@ -1,17 +1,25 @@
1
+ <% tab_list = capture do %>
2
+ <%= tag.ul class: classes, role: 'tablist', data: tab_list_data do %>
3
+ <% tabs.each do |tab| %><%= tab %><% end %>
4
+ <% if overflow_menu? %>
5
+ <li class="nav-item dropdown d-none" role="presentation" data-sdr-tab-overflow-target="more">
6
+ <button class="nav-link dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">More</button>
7
+ <ul class="dropdown-menu dropdown-menu-end" data-sdr-tab-overflow-target="menu"></ul>
8
+ </li>
9
+ <% end %>
10
+ <% end %>
11
+ <% end %>
12
+
1
13
  <% if collapse_below? %>
2
14
  <div data-controller="sdr-tab-select" data-action="shown.bs.tab->sdr-tab-select#sync">
3
- <%= tag.select class: select_classes, 'aria-label': 'Select a tab', data: { sdr_tab_select_target: 'select', action: 'sdr-tab-select#change' } do %>
15
+ <%= tag.select class: select_classes, 'aria-label': 'Select a tab', data: { sdr_tab_select_target: 'select', action: 'sdr-tab-select#change turbo:before-morph-element->sdr-tab-select#preventMorph' } do %>
4
16
  <% tabs.each do |tab| %><%= tag.option(tab.label, value: tab.id, selected: tab.active?) %><% end %>
5
17
  <% end %>
6
18
 
7
- <%= tag.ul class: classes, role: 'tablist' do %>
8
- <% tabs.each do |tab| %><%= tab %><% end %>
9
- <% end %>
19
+ <%= tab_list %>
10
20
  </div>
11
21
  <% else %>
12
- <%= tag.ul class: classes, role: 'tablist' do %>
13
- <% tabs.each do |tab| %><%= tab %><% end %>
14
- <% end %>
22
+ <%= tab_list %>
15
23
  <% end %>
16
24
 
17
25
  <%= header %>
@@ -19,11 +19,14 @@ module SdrViewComponents
19
19
  # fully functional (the `<select>` drives the same Bootstrap tab JavaScript) -- this
20
20
  # only changes how they're presented at narrow viewports. Leave nil (the default) to
21
21
  # always render the tabs.
22
- def initialize(classes: [], content_classes: [], variant: :default, collapse_below: nil)
22
+ # @param overflow_menu [Boolean] if true, tabs that don't fit on a single row are moved into a
23
+ # trailing "More" dropdown tab (by the `sdr-tab-overflow` controller).
24
+ def initialize(classes: [], content_classes: [], variant: :default, collapse_below: nil, overflow_menu: false)
23
25
  @classes = classes
24
26
  @content_classes = content_classes
25
27
  @variant = variant
26
28
  @collapse_below = collapse_below
29
+ @overflow_menu = overflow_menu
27
30
 
28
31
  raise ArgumentError, "Invalid variant: #{variant}" unless %i[underline default].include?(variant)
29
32
  if collapse_below && BREAKPOINTS.exclude?(collapse_below)
@@ -37,6 +40,17 @@ module SdrViewComponents
37
40
  @collapse_below.present?
38
41
  end
39
42
 
43
+ def overflow_menu?
44
+ @overflow_menu
45
+ end
46
+
47
+ def tab_list_data
48
+ return {} unless overflow_menu?
49
+
50
+ { controller: 'sdr-tab-overflow', action: 'shown.bs.tab->sdr-tab-overflow#syncMoreActive ' \
51
+ 'turbo:before-morph-element->sdr-tab-overflow#preventMorph' }
52
+ end
53
+
40
54
  def classes
41
55
  merge_classes('nav', @classes, variant_classes, collapse_below? ? ['d-none', "d-#{@collapse_below}-flex"] : nil)
42
56
  end
@@ -0,0 +1,20 @@
1
+ // Registers the gem's Stimulus controllers for Lookbook previews (see layouts/lookbook.html.erb).
2
+ import * as bootstrap from 'bootstrap'
3
+ import { Application } from '@hotwired/stimulus'
4
+ import TabErrorController from RAILS_ASSET_URL('./tab_error_controller.js')
5
+ import TabLinkController from RAILS_ASSET_URL('./tab_link_controller.js')
6
+ import TabNavController from RAILS_ASSET_URL('./tab_nav_controller.js')
7
+ import TabOverflowController from RAILS_ASSET_URL('./tab_overflow_controller.js')
8
+ import TabSelectController from RAILS_ASSET_URL('./tab_select_controller.js')
9
+ import ToastController from RAILS_ASSET_URL('./toast_controller.js')
10
+
11
+ // Previews' inline scripts use the global (e.g., to initialize tooltips).
12
+ window.bootstrap = bootstrap
13
+
14
+ const application = Application.start()
15
+ application.register('sdr-tab-error', TabErrorController)
16
+ application.register('sdr-tab-link', TabLinkController)
17
+ application.register('sdr-tab-nav', TabNavController)
18
+ application.register('sdr-tab-overflow', TabOverflowController)
19
+ application.register('sdr-tab-select', TabSelectController)
20
+ application.register('sdr-toast', ToastController)
@@ -0,0 +1,76 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ // Moves tabs that don't fit on a single row into a trailing "More" dropdown tab (see
4
+ // TabListComponent's `overflow_menu` option). The tab list is laid out again whenever
5
+ // its width changes.
6
+ //
7
+ // The tab list is excluded from Turbo morphs (e.g., page refreshes): morphing would move the
8
+ // tabs back to their server-rendered positions, close an open "More" menu, and replace the
9
+ // menu element out from under Bootstrap's Dropdown instance.
10
+ export default class extends Controller {
11
+ static targets = ['more', 'menu']
12
+
13
+ connect () {
14
+ this.resizeObserver = new window.ResizeObserver(() => {
15
+ if (this.element.offsetWidth === this.lastWidth) return
16
+
17
+ this.lastWidth = this.element.offsetWidth
18
+ this.layout()
19
+ })
20
+ this.resizeObserver.observe(this.element)
21
+ document.fonts?.ready.then(this.layout)
22
+ }
23
+
24
+ disconnect () {
25
+ this.resizeObserver.disconnect()
26
+ }
27
+
28
+ layout = () => {
29
+ this.restore()
30
+ const items = this.tabItems()
31
+ // Hidden (e.g., collapsed into a select) or everything fits on one row.
32
+ if (this.element.offsetWidth === 0 || items.length === 0) return
33
+ if (!this.isWrapped(items.at(-1), items[0])) return
34
+
35
+ this.moreTarget.classList.remove('d-none')
36
+ while (items.length > 1 && this.isWrapped(this.moreTarget, items[0])) {
37
+ this.overflow(items.pop())
38
+ }
39
+ this.syncMoreActive()
40
+ }
41
+
42
+ syncMoreActive = () => {
43
+ const toggle = this.moreTarget.querySelector('.dropdown-toggle')
44
+ toggle.classList.toggle('active', this.menuTarget.querySelector('.active') !== null)
45
+ }
46
+
47
+ preventMorph (event) {
48
+ if (event.target === this.element) event.preventDefault()
49
+ }
50
+
51
+ // Moves all tabs back into the tab list (in their original order) and hides "More".
52
+ restore () {
53
+ this.tabItems().forEach((item) => {
54
+ if (item.parentElement === this.element) return
55
+
56
+ this.element.insertBefore(item, this.moreTarget)
57
+ item.querySelector('[role="tab"]').classList.replace('dropdown-item', 'nav-link')
58
+ })
59
+ this.moreTarget.classList.add('d-none')
60
+ this.syncMoreActive()
61
+ }
62
+
63
+ overflow (item) {
64
+ this.menuTarget.prepend(item)
65
+ item.querySelector('[role="tab"]').classList.replace('nav-link', 'dropdown-item')
66
+ }
67
+
68
+ // The tab <li>s, in document order (tab list first, then the "More" menu).
69
+ tabItems () {
70
+ return Array.from(this.element.querySelectorAll('[role="tab"]'), (tab) => tab.closest('li'))
71
+ }
72
+
73
+ isWrapped (item, firstItem) {
74
+ return item.offsetTop > firstItem.offsetTop
75
+ }
76
+ }
@@ -1,22 +1,25 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
+ import * as bootstrap from 'bootstrap'
2
3
 
3
4
  // Drives a <select> that stands in for a tab list on narrow viewports (see
4
5
  // TabListComponent's `collapse_below` option). Selecting an option shows the
5
6
  // corresponding tab; activating a tab some other way (e.g. clicking the tab
6
7
  // list above the collapse breakpoint) keeps the select's value in sync.
7
8
  //
8
- // Uses window.bootstrap rather than `import * as bootstrap from 'bootstrap'`:
9
- // the UMD bootstrap.bundle.min.js has no ES module exports, so an ESM import
10
- // of it resolves to an empty namespace object even though the script itself
11
- // still assigns the real API to window.bootstrap as a side effect.
9
+ // The select is excluded from Turbo morphs (e.g., page refreshes), which would otherwise reset
10
+ // its value to the server-rendered selected option regardless of which tab is active.
12
11
  export default class extends Controller {
13
12
  static targets = ['select']
14
13
 
15
14
  change (event) {
16
- window.bootstrap.Tab.getOrCreateInstance(document.getElementById(event.target.value)).show()
15
+ bootstrap.Tab.getOrCreateInstance(document.getElementById(event.target.value)).show()
17
16
  }
18
17
 
19
18
  sync (event) {
20
19
  this.selectTarget.value = event.target.id
21
20
  }
21
+
22
+ preventMorph (event) {
23
+ if (event.target === this.selectTarget) event.preventDefault()
24
+ }
22
25
  }
@@ -34,12 +34,29 @@
34
34
 
35
35
  <%= stylesheet_link_tag 'sdr_view_components', 'data-turbo-track': 'reload' %>
36
36
  <%= tag.link rel: 'stylesheet', href: "#{SdrViewComponents.configuration.component_library_url}/styles/sul.css" %>
37
+ <%# Bootstrap is loaded as a module (rather than the UMD bundle) so the gem's Stimulus controllers can import it
38
+ without a second copy registering Bootstrap's data-API handlers twice. The import map must precede any module
39
+ script or preload, so it's in the head and the module isn't preloaded via a Link header. %>
40
+ <%= tag.script type: 'importmap', nonce: content_security_policy_nonce do %>
41
+ {
42
+ "imports": {
43
+ "@hotwired/stimulus": "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.js",
44
+ "bootstrap": "https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.esm.min.js",
45
+ "@popperjs/core": "https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/esm/index.js"
46
+ },
47
+ "integrity": {
48
+ "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.js": "sha384-Z9ISq680E84zQ7uFT7Pw8g/TlnoyCUECFycQz5aZs9IlrNFR56FhXoVi5U7bVi+l",
49
+ "https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.esm.min.js": "sha384-xBDFeCIxnhwUVfUb/XJFZZE66HuOkWrQ1ob02jFh5jhEJR9Kxx1KBEv3WvMColVg",
50
+ "https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/esm/index.js": "sha384-oIbyZ0FNSTCT8v7EkK3fFXaiJMESE5k4eUKo9fAynJEhjl0JzMYoJa5pTPQUP4+g"
51
+ }
52
+ }
53
+ <% end %>
54
+ <%= javascript_include_tag 'sdr_view_components/lookbook', type: 'module', nonce: true, preload_links_header: false %>
37
55
  </head>
38
56
 
39
57
  <body>
40
58
  <div class="container-fluid py-4">
41
59
  <%= yield %>
42
60
  </div>
43
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
44
61
  </body>
45
62
  </html>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SdrViewComponents
4
- VERSION = '0.12.1'
4
+ VERSION = '0.13.0'
5
5
  end
@@ -1,5 +1,4 @@
1
1
  <%# Resize the preview frame below the sm breakpoint to see the tabs collapse into a select. %>
2
- <%# The sdr-tab-select controller isn't registered in this dummy app, so the select won't switch panes here -- see the consuming app for a working example. %>
3
2
  <%= render SdrViewComponents::Elements::Tabs::TabListComponent.new(variant: :underline, collapse_below: :sm) do |tab_list| %>
4
3
  <% tab_list.with_tab(label: 'Details', id: 'details-tab', pane_id: 'details-pane', active: true) %>
5
4
  <% tab_list.with_tab(label: 'History', id: 'history-tab', pane_id: 'history-pane') %>
@@ -0,0 +1,10 @@
1
+ <%# Narrow the preview frame to see tabs that don't fit move into a "More" dropdown. %>
2
+ <% labels = %w[Overview Description Files History Events Metadata Workflows Versions Releases Tags] %>
3
+ <%= render SdrViewComponents::Elements::Tabs::TabListComponent.new(variant: :underline, overflow_menu: true) do |tab_list| %>
4
+ <% labels.each_with_index do |label, index| %>
5
+ <% tab_list.with_tab(label:, id: "#{label.downcase}-tab", pane_id: "#{label.downcase}-pane", active: index.zero?) %>
6
+ <% tab_list.with_pane(id: "#{label.downcase}-pane", tab_id: "#{label.downcase}-tab", active: index.zero?) do %>
7
+ <p><%= label %> pane content.</p>
8
+ <% end %>
9
+ <% end %>
10
+ <% end %>
@@ -12,6 +12,8 @@ module SdrViewComponents
12
12
  def with_header; end
13
13
 
14
14
  def collapsible; end
15
+
16
+ def overflow_menu; end
15
17
  end
16
18
  end
17
19
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sdr_view_components
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.1
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Collier
@@ -175,9 +175,11 @@ files:
175
175
  - app/components/sdr_view_components/tables/row_component.html.erb
176
176
  - app/components/sdr_view_components/tables/row_component.rb
177
177
  - app/components/sdr_view_components/tables/table_component.rb
178
+ - app/javascript/sdr_view_components/lookbook.js
178
179
  - app/javascript/sdr_view_components/tab_error_controller.js
179
180
  - app/javascript/sdr_view_components/tab_link_controller.js
180
181
  - app/javascript/sdr_view_components/tab_nav_controller.js
182
+ - app/javascript/sdr_view_components/tab_overflow_controller.js
181
183
  - app/javascript/sdr_view_components/tab_select_controller.js
182
184
  - app/javascript/sdr_view_components/toast_controller.js
183
185
  - app/views/layouts/lookbook.html.erb
@@ -214,6 +216,7 @@ files:
214
216
  - spec/components/previews/sdr_view_components/elements/tabs/tab_list_component_preview.rb
215
217
  - spec/components/previews/sdr_view_components/elements/tabs/tab_list_component_preview/collapsible.html.erb
216
218
  - spec/components/previews/sdr_view_components/elements/tabs/tab_list_component_preview/default_variant.html.erb
219
+ - spec/components/previews/sdr_view_components/elements/tabs/tab_list_component_preview/overflow_menu.html.erb
217
220
  - spec/components/previews/sdr_view_components/elements/tabs/tab_list_component_preview/underline_variant.html.erb
218
221
  - spec/components/previews/sdr_view_components/elements/tabs/tab_list_component_preview/with_header.html.erb
219
222
  - spec/components/previews/sdr_view_components/elements/toast_component_preview.rb