sdr_view_components 0.21.0 → 0.22.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: 9de1e10f3e04a4a00b584e85fa5b06d862916a2e403e7ab3966020e04ec3deb0
4
- data.tar.gz: 2df79a660409c1bed817b6823d81c6718d052026434611757f3883604b5d9411
3
+ metadata.gz: 1368eefb0da795ca19eb50623c50573654bc681bd419cd4f250a759aa1c969d9
4
+ data.tar.gz: 61d17dbefaeca0a568b845c2fc9872df5a484496cd1b9b07b18961e9d870afb5
5
5
  SHA512:
6
- metadata.gz: 94cd03e80e3fdca7e03e83456141569c53d888ec7a3ace284a6a0a9ceca187ae76471b859d3d5d3696b6311dbaa603e70f0516d4173532565cfd2ea81c6b306b
7
- data.tar.gz: 31b2838609b00ac1171421cf4481e82c6db92695309e38687cff917f2c585b983f60c2e7dda7500b496e31b38465e2c02f8f5876cb15480e983c8bdc30342e51
6
+ metadata.gz: d504ffc348734b15f10d607a2396f02e74e57922d3360ffd4b7c411b49156e1e5b1279997c21d1721bb1338c8a5ea18cfce4da7ee6995e27472cd5c3f466be6a
7
+ data.tar.gz: 59983cc56c2abfb208d2bfe3a05d5ff6656c93cdbb4e2c1ce9c9e8b54b7393ce2637b428703b4305aebbcacc249edadf3a14387849c9718db19a060668e59dde
data/README.md CHANGED
@@ -96,6 +96,19 @@ import TabOverflowController from "sdr_view_components/tab_overflow_controller"
96
96
  application.register("sdr-tab-overflow", TabOverflowController)
97
97
  ```
98
98
 
99
+ `SdrViewComponents::Tables::TreegridTableComponent` (see [Treegrid table component](#treegrid-table-component) below) ships `sdr_view_components/treegrid_controller`, registered as `sdr-treegrid`:
100
+
101
+ ```ruby
102
+ pin "sdr_view_components/treegrid_controller", to: "sdr_view_components/treegrid_controller.js"
103
+ ```
104
+
105
+ ```javascript
106
+ import { application } from "controllers/application"
107
+ import TreegridController from "sdr_view_components/treegrid_controller"
108
+
109
+ application.register("sdr-treegrid", TreegridController)
110
+ ```
111
+
99
112
  ## Usage
100
113
 
101
114
  ### Form components
@@ -250,6 +263,24 @@ Instead of `heading:`, the heading can be provided with the `heading_content` sl
250
263
  <% end %>
251
264
  ```
252
265
 
266
+ ### Treegrid table component
267
+
268
+ `SdrViewComponents::Tables::TreegridTableComponent` renders files as an [ARIA treegrid](https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/): a row for each file, preceded by a collapsible row for each folder in its path, with Expand all / Collapse all controls when there are folders. Provide files in path order (each folder's files before its subfolders), with the `path_parts` of each file's folder. It requires the `sdr-treegrid` controller (see [JavaScript](#javascript) above).
269
+
270
+ Folders are expanded unless `collapsed: true` is given. With `remember_expanded: true` (which requires an `id:`), folders the user expands or collapses stay so when the table is rendered again on the same page, e.g., when its turbo frame reloads. Enter on a file row activates the element in the row marked `data-sdr-treegrid-target="fileAction"`, if any.
271
+
272
+ ```erb
273
+ <%= render SdrViewComponents::Tables::TreegridTableComponent.new(label: 'Files', id: 'files', collapsed: true) do |component| %>
274
+ <% component.with_header(label: 'File/folder name') %>
275
+ <% component.with_header(label: 'Size') %>
276
+ <% files.each do |file| %>
277
+ <% component.with_file(path_parts: file.path_parts, label: file.filename) do |row| %>
278
+ <% row.with_cell { number_to_human_size(file.size) } %>
279
+ <% end %>
280
+ <% end %>
281
+ <% end %>
282
+ ```
283
+
253
284
  ### Navigation components
254
285
 
255
286
  SdrViewComponents provides components for building navigation bar items and dropdown menus:
@@ -216,6 +216,26 @@ table.table-data th {
216
216
  white-space: nowrap;
217
217
  }
218
218
 
219
+ /* Treegrid tables */
220
+ table.table-treegrid tr[data-sdr-treegrid-role="folder"] {
221
+ cursor: pointer;
222
+ }
223
+
224
+ table.table-treegrid .treegrid-name {
225
+ overflow-wrap: anywhere;
226
+ }
227
+
228
+ .treegrid-folder-icon {
229
+ display: inline-block;
230
+ margin-right: 0.25rem;
231
+ color: var(--bs-link-color);
232
+ transition: transform 0.2s ease-in-out;
233
+ }
234
+
235
+ table.table-treegrid tr[aria-expanded="false"] .treegrid-folder-icon {
236
+ transform: rotate(180deg);
237
+ }
238
+
219
239
  /* Radio buttons */
220
240
 
221
241
  /* Inputs / labels are rendered in the opposite order than BS styles expect. */
@@ -0,0 +1,7 @@
1
+ <tr class="treegrid-controls">
2
+ <%= tag.td colspan: do %>
3
+ <button type="button" class="btn btn-link p-0" data-action="sdr-treegrid#expandAll">Expand all</button>
4
+ <span aria-hidden="true">|</span>
5
+ <button type="button" class="btn btn-link p-0" data-action="sdr-treegrid#collapseAll">Collapse all</button>
6
+ <% end %>
7
+ </tr>
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Tables
5
+ # Component for rendering the Expand all / Collapse all row of a treegrid table.
6
+ class TreegridExpandCollapseRowComponent < BaseComponent
7
+ def initialize(colspan:)
8
+ @colspan = colspan
9
+ super()
10
+ end
11
+
12
+ attr_reader :colspan
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ <%= tag.tr role: 'row', tabindex: -1, hidden: hidden?, aria: { level: }, data: { sdr_treegrid_role: 'file' },
2
+ **row_options do %>
3
+ <%= tag.th scope: 'row', role: 'rowheader', class: 'treegrid-name fw-normal', style: indent_style do %>
4
+ <%= label || label_content %>
5
+ <% end %>
6
+ <% cells.each do |cell| %>
7
+ <%= cell %>
8
+ <% end %>
9
+ <% end %>
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Tables
5
+ # Component for rendering a file row of a treegrid table.
6
+ # Within the row, the element with data-sdr-treegrid-target="fileAction" (if any) is activated by Enter.
7
+ class TreegridFileRowComponent < TreegridRowComponent
8
+ renders_one :label_content
9
+ renders_many :cells, SdrViewComponents::Tables::CellComponent
10
+
11
+ # @param path_parts [Array<String>] the path of the file's folder (e.g., ['folder1', 'subfolder'])
12
+ # @param label [String] the file name; alternatively, provide label_content
13
+ def initialize(path_parts: [], label: nil, hidden: false, **row_options)
14
+ @label = label
15
+ @row_options = row_options
16
+ super(path_parts:, hidden:)
17
+ end
18
+
19
+ attr_reader :label, :row_options
20
+
21
+ def level
22
+ path_parts.size + 1
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ <%= tag.tr role: 'row', tabindex: -1, hidden: hidden?,
2
+ aria: { level:, expanded: expanded?.to_s },
3
+ data: { sdr_treegrid_role: 'folder', sdr_treegrid_key: key, action: 'click->sdr-treegrid#toggle' } do %>
4
+ <%= tag.th scope: 'row', role: 'rowheader', class: 'treegrid-name', style: indent_style do %>
5
+ <%= helpers.icon(icon_classes: 'bi bi-chevron-up', classes: 'treegrid-folder-icon', aria: { hidden: true }) %>
6
+ <span class="fw-bold"><%= label %></span>
7
+ <% end %>
8
+ <% empty_cells.times do %>
9
+ <td></td>
10
+ <% end %>
11
+ <% end %>
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Tables
5
+ # Component for rendering a folder row of a treegrid table.
6
+ class TreegridFolderRowComponent < TreegridRowComponent
7
+ # @param path_parts [Array<String>] the folder's path (e.g., ['folder1', 'subfolder'])
8
+ # @param empty_cells [Integer] the number of empty cells following the folder name
9
+ def initialize(path_parts:, expanded:, empty_cells:, hidden: false)
10
+ @expanded = expanded
11
+ @empty_cells = empty_cells
12
+ super(path_parts:, hidden:)
13
+ end
14
+
15
+ attr_reader :empty_cells
16
+
17
+ def expanded?
18
+ @expanded
19
+ end
20
+
21
+ def level
22
+ path_parts.size
23
+ end
24
+
25
+ def label
26
+ path_parts.last
27
+ end
28
+
29
+ # Identifies the folder when the table is rendered again.
30
+ def key
31
+ path_parts.join('/')
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Tables
5
+ # Base component for the folder and file rows of a treegrid table.
6
+ class TreegridRowComponent < BaseComponent
7
+ def initialize(path_parts:, hidden: false)
8
+ @path_parts = path_parts
9
+ @hidden = hidden
10
+ super()
11
+ end
12
+
13
+ attr_reader :path_parts
14
+
15
+ def hidden?
16
+ @hidden
17
+ end
18
+
19
+ # Subclasses must provide level (the row's depth in the tree, starting at 1).
20
+ def indent_style
21
+ "padding-left: #{((level - 1) * 1.25) + 0.5}rem;"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Tables
5
+ # Component for rendering files as a treegrid table, with a collapsible row for each folder in their paths.
6
+ # Requires the sdr-treegrid Stimulus controller.
7
+ class TreegridTableComponent < BaseTableComponent
8
+ # Files must be provided in path order, so that each folder's files and subfolders follow its row.
9
+ renders_many :files, lambda { |path_parts: [], **args|
10
+ TreegridFileRowComponent.new(path_parts:, hidden: collapsed? && path_parts.any?, **args)
11
+ }
12
+
13
+ # @param collapsed [Boolean] whether folders are initially collapsed
14
+ # @param remember_expanded [Boolean] whether folders the user expands or collapses stay so when the table
15
+ # is rendered again (e.g., when its turbo frame reloads); requires an id
16
+ def initialize(collapsed: false, remember_expanded: false, classes: [], data: {}, **args)
17
+ raise ArgumentError, 'remember_expanded requires an id' if remember_expanded && args[:id].blank?
18
+
19
+ @collapsed = collapsed
20
+ super(classes: merge_classes('table-treegrid', classes),
21
+ role: 'treegrid',
22
+ data: data.merge(controller: merge_classes('sdr-treegrid', data[:controller]),
23
+ action: merge_actions('keydown->sdr-treegrid#navigate', data[:action]),
24
+ sdr_treegrid_remember_expanded_value: remember_expanded),
25
+ **args)
26
+ end
27
+
28
+ def collapsed?
29
+ @collapsed
30
+ end
31
+
32
+ def rows?
33
+ files?
34
+ end
35
+
36
+ # Folder rows are inserted before the first file in each folder.
37
+ def rows
38
+ @rows ||= [expand_collapse_row, *file_and_folder_rows].compact
39
+ end
40
+
41
+ private
42
+
43
+ def file_and_folder_rows
44
+ last_path_parts = []
45
+ files.flat_map do |file|
46
+ folder_rows = new_folder_path_parts(last_path_parts:, path_parts: file.path_parts).map do |folder_path_parts|
47
+ folder_row(folder_path_parts)
48
+ end
49
+ last_path_parts = file.path_parts
50
+ [*folder_rows, file]
51
+ end
52
+ end
53
+
54
+ # @return [Array<Array<String>>] the path parts of each folder in path_parts that is not in last_path_parts
55
+ def new_folder_path_parts(last_path_parts:, path_parts:)
56
+ matching_count = path_parts.zip(last_path_parts).take_while { |part, last_part| part == last_part }.size
57
+ (matching_count...path_parts.size).map { |index| path_parts[0..index] }
58
+ end
59
+
60
+ def folder_row(folder_path_parts)
61
+ render TreegridFolderRowComponent.new(path_parts: folder_path_parts,
62
+ expanded: !collapsed?,
63
+ hidden: collapsed? && folder_path_parts.size > 1,
64
+ empty_cells: column_count - 1)
65
+ end
66
+
67
+ def expand_collapse_row
68
+ return if files.none? { |file| file.path_parts.any? }
69
+
70
+ render TreegridExpandCollapseRowComponent.new(colspan: column_count)
71
+ end
72
+
73
+ def column_count
74
+ [headers.size, 1].max
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,193 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Expanded state of the folders that users have toggled, by table id then folder key, kept across renders.
4
+ const expandedByTable = new Map()
5
+
6
+ // Expands and collapses the folder rows of a treegrid table (TreegridTableComponent),
7
+ // with keyboard navigation per https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/
8
+ // Only one row (the last focused) is in the tab order.
9
+ export default class extends Controller {
10
+ static targets = ["fileAction"]
11
+ static values = { rememberExpanded: Boolean }
12
+
13
+ connect() {
14
+ if (this.rememberExpandedValue) this.restoreExpanded()
15
+ this.element.addEventListener("focusin", this.focusRow)
16
+ this.setTabbableRow(this.visibleRows()[0])
17
+ }
18
+
19
+ disconnect() {
20
+ this.element.removeEventListener("focusin", this.focusRow)
21
+ }
22
+
23
+ focusRow = (event) => {
24
+ if (this.isTreeRow(event.target)) this.setTabbableRow(event.target)
25
+ }
26
+
27
+ setTabbableRow(row) {
28
+ if (!row) return
29
+
30
+ this.element.querySelectorAll(':scope > tbody > tr[tabindex="0"]').forEach((other) => { other.tabIndex = -1 })
31
+ row.tabIndex = 0
32
+ }
33
+
34
+ toggle(event) {
35
+ this.toggleFolder(event.target.closest("tr"))
36
+ }
37
+
38
+ expandAll(event) {
39
+ event.preventDefault()
40
+ this.folderRows().forEach((row) => {
41
+ if (!this.isOpenFolder(row)) this.toggleFolder(row)
42
+ })
43
+ }
44
+
45
+ collapseAll(event) {
46
+ event.preventDefault()
47
+ this.folderRows().forEach((row) => {
48
+ if (this.isOpenFolder(row)) this.toggleFolder(row)
49
+ })
50
+ }
51
+
52
+ navigate(event) {
53
+ const row = event.target
54
+ if (!this.isTreeRow(row)) return
55
+
56
+ const target = this.navigationTarget(event.key, row)
57
+ if (target === undefined) return
58
+
59
+ event.preventDefault()
60
+ if (typeof target === "function") {
61
+ target()
62
+ } else {
63
+ target?.focus()
64
+ }
65
+ }
66
+
67
+ // @return [Function, Element, null, undefined] an action or the row to focus; undefined if the key isn't handled
68
+ navigationTarget(key, row) {
69
+ switch (key) {
70
+ case "Enter":
71
+ if (this.isFolder(row)) return () => this.toggleFolder(row)
72
+ return this.fileAction(row) ? () => this.fileAction(row).click() : undefined
73
+ case "ArrowRight":
74
+ if (!this.isFolder(row)) return undefined
75
+ return this.isOpenFolder(row) ? this.nextVisibleRow(row) : () => this.toggleFolder(row)
76
+ case "ArrowLeft":
77
+ if (this.isOpenFolder(row)) return () => this.toggleFolder(row)
78
+ return this.level(row) > 1 ? this.parentRow(row) : undefined
79
+ case "ArrowUp":
80
+ return this.previousVisibleRow(row)
81
+ case "ArrowDown":
82
+ return this.nextVisibleRow(row)
83
+ case "Home":
84
+ return this.visibleRows()[0]
85
+ case "End":
86
+ return this.visibleRows().at(-1)
87
+ default:
88
+ return undefined
89
+ }
90
+ }
91
+
92
+ toggleFolder(row) {
93
+ const expand = !this.isOpenFolder(row)
94
+ row.setAttribute("aria-expanded", expand)
95
+ if (this.rememberExpandedValue) this.expandedFolders().set(row.dataset.sdrTreegridKey, expand)
96
+
97
+ if (expand) {
98
+ this.showDescendantRows(row)
99
+ } else {
100
+ const descendants = this.descendantRows(row)
101
+ descendants.forEach((descendant) => { descendant.hidden = true })
102
+ // Keeps a row in the tab order when the tabbable row is hidden.
103
+ if (descendants.some((descendant) => descendant.tabIndex === 0)) this.setTabbableRow(row)
104
+ }
105
+ }
106
+
107
+ // Shows the descendants of an expanded folder, except those within collapsed subfolders.
108
+ showDescendantRows(row) {
109
+ let collapsedLevel = null
110
+ this.descendantRows(row).forEach((descendant) => {
111
+ if (collapsedLevel !== null && this.level(descendant) > collapsedLevel) return
112
+
113
+ descendant.hidden = false
114
+ collapsedLevel = this.isFolder(descendant) && !this.isOpenFolder(descendant) ? this.level(descendant) : null
115
+ })
116
+ }
117
+
118
+ // Rows are in tree order, so a folder within a collapsed folder is only marked expanded.
119
+ restoreExpanded() {
120
+ this.folderRows().forEach((row) => {
121
+ const expanded = this.expandedFolders().get(row.dataset.sdrTreegridKey)
122
+ if (expanded === undefined || expanded === this.isOpenFolder(row)) return
123
+
124
+ if (expanded && row.hidden) {
125
+ row.setAttribute("aria-expanded", true)
126
+ } else {
127
+ this.toggleFolder(row)
128
+ }
129
+ })
130
+ }
131
+
132
+ expandedFolders() {
133
+ if (!expandedByTable.has(this.element.id)) expandedByTable.set(this.element.id, new Map())
134
+ return expandedByTable.get(this.element.id)
135
+ }
136
+
137
+ descendantRows(row) {
138
+ const descendants = []
139
+ let sibling = row.nextElementSibling
140
+ while (sibling && this.isTreeRow(sibling) && this.level(sibling) > this.level(row)) {
141
+ descendants.push(sibling)
142
+ sibling = sibling.nextElementSibling
143
+ }
144
+ return descendants
145
+ }
146
+
147
+ parentRow(row) {
148
+ let sibling = row.previousElementSibling
149
+ while (sibling && this.isTreeRow(sibling)) {
150
+ if (this.level(sibling) === this.level(row) - 1) return sibling
151
+ sibling = sibling.previousElementSibling
152
+ }
153
+ return null
154
+ }
155
+
156
+ nextVisibleRow(row) {
157
+ const rows = this.visibleRows()
158
+ return rows[rows.indexOf(row) + 1] || null
159
+ }
160
+
161
+ previousVisibleRow(row) {
162
+ const rows = this.visibleRows()
163
+ return rows[rows.indexOf(row) - 1] || null
164
+ }
165
+
166
+ visibleRows() {
167
+ return Array.from(this.element.querySelectorAll(":scope > tbody > tr[aria-level]:not([hidden])"))
168
+ }
169
+
170
+ folderRows() {
171
+ return this.element.querySelectorAll(':scope > tbody > tr[data-sdr-treegrid-role="folder"]')
172
+ }
173
+
174
+ fileAction(row) {
175
+ return this.fileActionTargets.find((target) => row.contains(target))
176
+ }
177
+
178
+ isTreeRow(element) {
179
+ return element.matches("tr[aria-level]")
180
+ }
181
+
182
+ isFolder(row) {
183
+ return row.dataset.sdrTreegridRole === "folder"
184
+ }
185
+
186
+ isOpenFolder(row) {
187
+ return this.isFolder(row) && row.getAttribute("aria-expanded") === "true"
188
+ }
189
+
190
+ level(row) {
191
+ return Number(row.getAttribute("aria-level"))
192
+ }
193
+ }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SdrViewComponents
4
- VERSION = '0.21.0'
4
+ VERSION = '0.22.0'
5
5
  end
@@ -0,0 +1,10 @@
1
+ <%= render(SdrViewComponents::Tables::TreegridTableComponent.new(label: 'Files', id: 'preview-files', collapsed:,
2
+ remember_expanded: true)) do |component| %>
3
+ <% component.with_header(label: 'File/folder name') %>
4
+ <% component.with_header(label: 'Size') %>
5
+ <% files.each do |file| %>
6
+ <% component.with_file(path_parts: file[:path_parts], label: file[:label]) do |row| %>
7
+ <% row.with_cell { file[:size] } %>
8
+ <% end %>
9
+ <% end %>
10
+ <% end %>
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrViewComponents
4
+ module Tables
5
+ class TreegridTableComponentPreview < ViewComponent::Preview
6
+ FILES = [
7
+ { path_parts: [], label: 'README.txt', size: '1 KB' },
8
+ { path_parts: %w[images], label: 'image1.tif', size: '10 MB' },
9
+ { path_parts: %w[images], label: 'image2.tif', size: '12 MB' },
10
+ { path_parts: %w[images thumbnails], label: 'image1.jpg', size: '20 KB' },
11
+ { path_parts: %w[data], label: 'data.csv', size: '3 MB' }
12
+ ].freeze
13
+
14
+ def default
15
+ render_with_template(locals: { files: FILES, collapsed: false })
16
+ end
17
+
18
+ def collapsed
19
+ render_with_template(template: 'sdr_view_components/tables/treegrid_table_component_preview/default',
20
+ locals: { files: FILES, collapsed: true })
21
+ end
22
+ end
23
+ end
24
+ 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.21.0
4
+ version: 0.22.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Collier
@@ -180,6 +180,14 @@ files:
180
180
  - app/components/sdr_view_components/tables/row_component.html.erb
181
181
  - app/components/sdr_view_components/tables/row_component.rb
182
182
  - app/components/sdr_view_components/tables/table_component.rb
183
+ - app/components/sdr_view_components/tables/treegrid_expand_collapse_row_component.html.erb
184
+ - app/components/sdr_view_components/tables/treegrid_expand_collapse_row_component.rb
185
+ - app/components/sdr_view_components/tables/treegrid_file_row_component.html.erb
186
+ - app/components/sdr_view_components/tables/treegrid_file_row_component.rb
187
+ - app/components/sdr_view_components/tables/treegrid_folder_row_component.html.erb
188
+ - app/components/sdr_view_components/tables/treegrid_folder_row_component.rb
189
+ - app/components/sdr_view_components/tables/treegrid_row_component.rb
190
+ - app/components/sdr_view_components/tables/treegrid_table_component.rb
183
191
  - app/javascript/sdr_view_components/lookbook.js
184
192
  - app/javascript/sdr_view_components/tab_error_controller.js
185
193
  - app/javascript/sdr_view_components/tab_link_controller.js
@@ -187,6 +195,7 @@ files:
187
195
  - app/javascript/sdr_view_components/tab_overflow_controller.js
188
196
  - app/javascript/sdr_view_components/tab_select_controller.js
189
197
  - app/javascript/sdr_view_components/toast_controller.js
198
+ - app/javascript/sdr_view_components/treegrid_controller.js
190
199
  - app/views/layouts/lookbook.html.erb
191
200
  - config/routes.rb
192
201
  - lib/sdr_view_components.rb
@@ -368,6 +377,8 @@ files:
368
377
  - spec/components/previews/sdr_view_components/tables/row_component_preview/with_value.html.erb
369
378
  - spec/components/previews/sdr_view_components/tables/row_component_preview/with_values.html.erb
370
379
  - spec/components/previews/sdr_view_components/tables/table_component_preview.rb
380
+ - spec/components/previews/sdr_view_components/tables/treegrid_table_component_preview.rb
381
+ - spec/components/previews/sdr_view_components/tables/treegrid_table_component_preview/default.html.erb
371
382
  homepage: https://github.com/sul-dlss/sdr_view_components
372
383
  licenses: []
373
384
  metadata: