playbook_ui 16.3.0 → 16.4.0.pre.alpha.PLAY2846reactadvancedtablecalcheaderpinnedrows15356

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 (148) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/Components/CustomCell.tsx +17 -4
  3. data/app/pb_kits/playbook/pb_advanced_table/Components/TableHeaderCell.tsx +3 -1
  4. data/app/pb_kits/playbook/pb_advanced_table/Context/AdvancedTableContext.tsx +5 -2
  5. data/app/pb_kits/playbook/pb_advanced_table/Hooks/useTableActions.ts +21 -9
  6. data/app/pb_kits/playbook/pb_advanced_table/Hooks/useTableState.ts +5 -2
  7. data/app/pb_kits/playbook/pb_advanced_table/SubKits/TableHeader.tsx +9 -11
  8. data/app/pb_kits/playbook/pb_advanced_table/Utilities/ExpansionControlHelpers.tsx +25 -1
  9. data/app/pb_kits/playbook/pb_advanced_table/Utilities/RowModelUtils.ts +100 -0
  10. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.scss +65 -1
  11. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.tsx +12 -2
  12. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.html.erb +2 -2
  13. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.rb +9 -0
  14. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx +109 -2
  15. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_cascade_collapse.jsx +50 -0
  16. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_cascade_collapse.md +1 -0
  17. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows_rails.html.erb +57 -0
  18. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows_rails.md +7 -0
  19. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_sort_parent_only.jsx +175 -0
  20. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_sort_parent_only.md +5 -0
  21. data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +3 -0
  22. data/app/pb_kits/playbook/pb_advanced_table/docs/index.js +3 -1
  23. data/app/pb_kits/playbook/pb_advanced_table/index.js +130 -29
  24. data/app/pb_kits/playbook/pb_advanced_table/scss_partials/advanced_table_sticky_mixin.scss +6 -2
  25. data/app/pb_kits/playbook/pb_advanced_table/table_body.html.erb +21 -4
  26. data/app/pb_kits/playbook/pb_advanced_table/table_body.rb +115 -9
  27. data/app/pb_kits/playbook/pb_advanced_table/table_row.html.erb +3 -1
  28. data/app/pb_kits/playbook/pb_advanced_table/table_row.rb +12 -1
  29. data/app/pb_kits/playbook/pb_advanced_table/table_subrow_header.html.erb +4 -1
  30. data/app/pb_kits/playbook/pb_advanced_table/table_subrow_header.rb +9 -1
  31. data/app/pb_kits/playbook/pb_button/_button_mixins.scss +6 -1
  32. data/app/pb_kits/playbook/pb_button/docs/_button_full_width_rails.md +19 -0
  33. data/app/pb_kits/playbook/pb_button/docs/_button_full_width_react.md +23 -0
  34. data/app/pb_kits/playbook/pb_circle_icon_button/_circle_icon_button.scss +5 -0
  35. data/app/pb_kits/playbook/pb_collapsible/index.js +15 -26
  36. data/app/pb_kits/playbook/pb_date_picker/date_picker_helper.ts +3 -1
  37. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_compound_components.html.erb +1 -1
  38. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_compound_components.jsx +6 -3
  39. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_full_height.html.erb +3 -3
  40. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_full_height.jsx +6 -3
  41. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_full_height_placement.html.erb +3 -3
  42. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_full_height_placement.jsx +6 -3
  43. data/app/pb_kits/playbook/pb_dropdown/_dropdown.scss +3 -0
  44. data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +1 -0
  45. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_closing_options_rails.html.erb +16 -0
  46. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_closing_options_rails.md +1 -0
  47. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_custom_event_type.html.erb +224 -0
  48. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_custom_event_type.md +7 -0
  49. data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +2 -0
  50. data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +8 -1
  51. data/app/pb_kits/playbook/pb_dropdown/index.js +255 -46
  52. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownTrigger.tsx +19 -14
  53. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/_fixed_confirmation_toast.scss +4 -0
  54. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/_fixed_confirmation_toast.tsx +3 -0
  55. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_nav_margin.html.erb +46 -0
  56. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_nav_margin.jsx +42 -0
  57. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_nav_margin_rails.md +1 -0
  58. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_nav_margin_react.md +1 -0
  59. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/example.yml +2 -0
  60. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/index.js +2 -1
  61. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/fixed_confirmation_toast.rb +7 -1
  62. data/app/pb_kits/playbook/pb_icon/icon.rb +7 -1
  63. data/app/pb_kits/playbook/pb_list/_list_mixin.scss +4 -4
  64. data/app/pb_kits/playbook/pb_multi_level_select/_helper_functions.tsx +1 -1
  65. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +27 -16
  66. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_placeholder.html.erb +109 -0
  67. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_placeholder.jsx +127 -0
  68. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_placeholder.md +1 -0
  69. data/app/pb_kits/playbook/pb_multi_level_select/docs/example.yml +2 -0
  70. data/app/pb_kits/playbook/pb_multi_level_select/docs/index.js +1 -0
  71. data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.rb +3 -0
  72. data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.test.jsx +27 -0
  73. data/app/pb_kits/playbook/pb_popover/docs/_popover_placement.jsx +81 -0
  74. data/app/pb_kits/playbook/pb_popover/docs/_popover_placement_react.md +1 -0
  75. data/app/pb_kits/playbook/pb_popover/docs/_popover_position.html.erb +128 -0
  76. data/app/pb_kits/playbook/pb_popover/docs/_popover_position_rails.md +1 -0
  77. data/app/pb_kits/playbook/pb_popover/docs/example.yml +2 -0
  78. data/app/pb_kits/playbook/pb_popover/docs/index.js +2 -1
  79. data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.tsx +35 -134
  80. data/app/pb_kits/playbook/pb_rich_text_editor/_tiptap_editor.tsx +51 -0
  81. data/app/pb_kits/playbook/pb_rich_text_editor/_trix_editor.tsx +206 -0
  82. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_default.jsx +56 -0
  83. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_default.md +1 -0
  84. data/app/pb_kits/playbook/pb_rich_text_editor/docs/example.yml +13 -21
  85. data/app/pb_kits/playbook/pb_rich_text_editor/docs/index.js +0 -10
  86. data/app/pb_kits/playbook/pb_rich_text_editor/inlineFocus.ts +5 -4
  87. data/app/pb_kits/playbook/pb_selectable_list/_selectable_list.scss +19 -1
  88. data/app/pb_kits/playbook/pb_table/_table.tsx +24 -21
  89. data/app/pb_kits/playbook/pb_table/docs/_sections.yml +1 -0
  90. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_external_filter_rails.html.erb +45 -0
  91. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_external_filter_rails.md +39 -0
  92. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_rails.md +2 -1
  93. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_with_card_title_props.jsx +152 -0
  94. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_with_card_title_props.md +17 -0
  95. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_with_card_title_props_rails.html.erb +121 -0
  96. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_with_card_title_props_rails.md +17 -0
  97. data/app/pb_kits/playbook/pb_table/docs/example.yml +3 -0
  98. data/app/pb_kits/playbook/pb_table/docs/index.js +1 -0
  99. data/app/pb_kits/playbook/pb_table/table.html.erb +17 -13
  100. data/app/pb_kits/playbook/pb_table/table.rb +8 -0
  101. data/app/pb_kits/playbook/pb_table/table.test.js +33 -0
  102. data/app/pb_kits/playbook/pb_textarea/_textarea.scss +4 -1
  103. data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +105 -3
  104. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_with_highlight.jsx +20 -8
  105. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_with_highlight.md +3 -0
  106. data/app/pb_kits/playbook/utilities/_hover.scss +6 -3
  107. data/app/pb_kits/playbook/utilities/domHelpers.ts +50 -0
  108. data/dist/chunks/{_pb_line_graph-CKBPxTmM.js → _pb_line_graph-D6s5rymw.js} +1 -1
  109. data/dist/chunks/_typeahead-BNp_YiTh.js +1 -0
  110. data/dist/chunks/componentRegistry-DRSp5D_e.js +1 -0
  111. data/dist/chunks/{globalProps-DLCfJwiU.js → globalProps-Ds_6HBhX.js} +1 -1
  112. data/dist/chunks/lib-BaO72ugL.js +29 -0
  113. data/dist/chunks/vendor.js +5 -5
  114. data/dist/menu.yml +3 -2
  115. data/dist/playbook-rails-react-bindings.js +1 -1
  116. data/dist/playbook-rails.js +1 -1
  117. data/dist/playbook.css +1 -1
  118. data/lib/playbook/pb_forms_helper.rb +3 -0
  119. data/lib/playbook/version.rb +2 -2
  120. metadata +42 -32
  121. data/app/pb_kits/playbook/pb_button/docs/_button_full_width.md +0 -1
  122. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_attributes.html.erb +0 -5
  123. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_attributes.jsx +0 -15
  124. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_default.html.erb +0 -1
  125. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_focus.html.erb +0 -3
  126. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_focus.jsx +0 -17
  127. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_inline.html.erb +0 -6
  128. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_inline.jsx +0 -16
  129. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_label.jsx +0 -28
  130. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_label.md +0 -1
  131. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_preview.html.erb +0 -35
  132. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_preview.jsx +0 -45
  133. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_required_indicator.html.erb +0 -10
  134. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_required_indicator.jsx +0 -22
  135. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_required_indicator.md +0 -3
  136. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_simple.html.erb +0 -1
  137. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_simple.jsx +0 -13
  138. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_sticky.html.erb +0 -1
  139. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_sticky.jsx +0 -15
  140. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_templates.html.erb +0 -115
  141. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_templates.jsx +0 -42
  142. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_toolbar_bottom.html.erb +0 -4
  143. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_toolbar_bottom.jsx +0 -14
  144. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.html.erb +0 -5
  145. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.rb +0 -63
  146. data/dist/chunks/_typeahead-B7bktFm6.js +0 -1
  147. data/dist/chunks/componentRegistry-DzmmLR2x.js +0 -1
  148. data/dist/chunks/lib-QT_7rPYf.js +0 -29
@@ -1,5 +1,6 @@
1
1
  import PbEnhancedElement from "../pb_enhanced_element";
2
2
  import { updateSelectionActionBar } from "./advanced_table_action_bar";
3
+ import { setArrowVisibility, toggleVisibility } from "../utilities/domHelpers";
3
4
 
4
5
  const ADVANCED_TABLE_SELECTOR = "[data-advanced-table]";
5
6
  const DOWN_ARROW_SELECTOR = "#advanced-table_open_icon";
@@ -20,10 +21,18 @@ export default class PbAdvancedTable extends PbEnhancedElement {
20
21
  this.childRowsMap = new Map();
21
22
  }
22
23
 
24
+ get table() {
25
+ return this.cachedTable || (this.cachedTable = this.element.closest("table"));
26
+ }
27
+
28
+ get mainTable() {
29
+ return this.cachedMainTable || (this.cachedMainTable = this.element.closest(".pb_advanced_table"));
30
+ }
31
+
23
32
  // Fetch and cache child rows for a given parent row ID
24
33
  childRowsFor(parentId) {
25
34
  if (!this.childRowsMap.has(parentId)) {
26
- const table = this.element.closest("table");
35
+ const table = this.table;
27
36
  const rows = Array.from(
28
37
  table.querySelectorAll(`tr[data-row-parent="${parentId}"]`)
29
38
  );
@@ -33,7 +42,8 @@ export default class PbAdvancedTable extends PbEnhancedElement {
33
42
  }
34
43
 
35
44
  updateTableSelectedRowsAttribute() {
36
- const mainTable = this.element.closest(".pb_advanced_table");
45
+ const mainTable = this.mainTable;
46
+ if (!mainTable) return;
37
47
  mainTable.dataset.selectedRows = JSON.stringify(
38
48
  Array.from(PbAdvancedTable.selectedRows)
39
49
  );
@@ -41,7 +51,8 @@ export default class PbAdvancedTable extends PbEnhancedElement {
41
51
 
42
52
  // Recalculate selected count based on all checked checkboxes
43
53
  recalculateSelectedCount() {
44
- const table = this.element.closest("table");
54
+ const table = this.table;
55
+ if (!table) return;
45
56
 
46
57
  // Get all checkboxes that could be part of the selection
47
58
  // This includes row checkboxes and any parent checkboxes that might be programmatically checked
@@ -95,7 +106,7 @@ export default class PbAdvancedTable extends PbEnhancedElement {
95
106
  });
96
107
 
97
108
  this.updateTableSelectedRowsAttribute();
98
- updateSelectionActionBar(table.closest(".pb_advanced_table"), PbAdvancedTable.selectedRows.size);
109
+ updateSelectionActionBar(this.mainTable, PbAdvancedTable.selectedRows.size);
99
110
 
100
111
  // Sync header select-all state
101
112
  if (selectAllCheckbox) {
@@ -139,7 +150,7 @@ export default class PbAdvancedTable extends PbEnhancedElement {
139
150
 
140
151
  this.updateTableSelectedRowsAttribute();
141
152
 
142
- const table = checkbox.closest("table");
153
+ const table = this.table;
143
154
  const selectAllCheckbox = table.querySelector("#select-all-rows");
144
155
 
145
156
  if (selectAllCheckbox) {
@@ -153,7 +164,7 @@ export default class PbAdvancedTable extends PbEnhancedElement {
153
164
  );
154
165
  selectAllInput.checked = allChecked;
155
166
  }
156
- updateSelectionActionBar(table.closest(".pb_advanced_table"), PbAdvancedTable.selectedRows.size);
167
+ updateSelectionActionBar(this.mainTable, PbAdvancedTable.selectedRows.size);
157
168
  }
158
169
 
159
170
  get target() {
@@ -161,10 +172,11 @@ export default class PbAdvancedTable extends PbEnhancedElement {
161
172
  }
162
173
 
163
174
  connect() {
164
- const table = this.element.closest("table");
175
+ const table = this.table;
176
+ if (!table) return;
165
177
 
166
178
  this.hideCloseIcon();
167
- const mainTable = this.element.closest(".pb_advanced_table");
179
+ const mainTable = this.mainTable;
168
180
 
169
181
  // This so it is hidden on first render
170
182
  if (mainTable) {
@@ -184,6 +196,17 @@ export default class PbAdvancedTable extends PbEnhancedElement {
184
196
  if (table.dataset.pbAdvancedTableInitialized) return;
185
197
  table.dataset.pbAdvancedTableInitialized = "true";
186
198
 
199
+ // Measure header height so pinned rows don't overlap when header wraps (e.g. mobile)
200
+ if (mainTable) {
201
+ PbAdvancedTable.updateStickyHeaderRowHeights(mainTable);
202
+ const resizeObserver = new ResizeObserver(() => {
203
+ PbAdvancedTable.updateStickyHeaderRowHeights(mainTable);
204
+ PbAdvancedTable.updatePinnedRowsStickyTops(mainTable);
205
+ });
206
+ resizeObserver.observe(table);
207
+ mainTable._advancedTableHeaderResizeObserver = resizeObserver;
208
+ }
209
+
187
210
  // Delegate checkbox changes
188
211
  table.addEventListener("change", (event) => {
189
212
  const checkbox = event.target.closest('input[type="checkbox"]');
@@ -271,9 +294,7 @@ export default class PbAdvancedTable extends PbEnhancedElement {
271
294
  }
272
295
 
273
296
  // Find direct child rows
274
- const childRows = Array.from(
275
- table.querySelectorAll(`[data-row-parent="${toggleBtn.id}"]`)
276
- );
297
+ const childRows = this.childRowsFor(toggleBtn.id);
277
298
  this.toggleElement(childRows);
278
299
 
279
300
  // Restore original element context
@@ -284,7 +305,8 @@ export default class PbAdvancedTable extends PbEnhancedElement {
284
305
  }
285
306
 
286
307
  addBorderRadiusOnLastVisibleRow() {
287
- const parentElement = this.element.closest(".pb_advanced_table");
308
+ const parentElement = this.mainTable;
309
+ if (!parentElement) return;
288
310
 
289
311
  const table = document.getElementById(parentElement.id);
290
312
 
@@ -304,9 +326,64 @@ export default class PbAdvancedTable extends PbEnhancedElement {
304
326
  lastVisibleRow.classList.add("last-visible-row");
305
327
  lastVisibleRow.classList.add("last-row-cell");
306
328
  }
329
+
330
+ PbAdvancedTable.updateStickyHeaderRowHeights(parentElement);
331
+ PbAdvancedTable.updatePinnedRowsStickyTops(table);
307
332
  }
308
333
  }
309
334
 
335
+ /**
336
+ * Measure thead height and set --advanced-table-header-height so pinned rows and
337
+ * multi-row sticky headers use the correct offset. Re-run when header wraps (e.g. mobile).
338
+ */
339
+ static updateStickyHeaderRowHeights(advancedTableWrapper) {
340
+ if (!advancedTableWrapper) return;
341
+ const table = advancedTableWrapper.querySelector("table.pb_table");
342
+ const thead = table?.querySelector("thead");
343
+ if (!thead) return;
344
+
345
+ const rows = Array.from(thead.querySelectorAll("tr"));
346
+ let totalHeight = 0;
347
+ rows.forEach((tr, index) => {
348
+ const h = tr.offsetHeight;
349
+ if (index === 0) {
350
+ advancedTableWrapper.style.setProperty(
351
+ "--advanced-table-header-row-0-height",
352
+ `${h}px`
353
+ );
354
+ } else if (index === 1) {
355
+ advancedTableWrapper.style.setProperty(
356
+ "--advanced-table-header-row-1-height",
357
+ `${h}px`
358
+ );
359
+ }
360
+ totalHeight += h;
361
+ });
362
+ advancedTableWrapper.style.setProperty(
363
+ "--advanced-table-header-height",
364
+ `${totalHeight}px`
365
+ );
366
+ }
367
+
368
+ /**
369
+ * Recompute sticky top for visible pinned rows so collapsed rows don't leave a gap.
370
+ * Call after expand/collapse and on load.
371
+ */
372
+ static updatePinnedRowsStickyTops(advancedTableWrapper) {
373
+ const pinnedTbody = advancedTableWrapper?.querySelector("tbody.pinned-rows-tbody");
374
+ if (!pinnedTbody) return;
375
+
376
+ const pinnedRows = Array.from(pinnedTbody.querySelectorAll("tr.pinned-row"));
377
+ const visibleRows = pinnedRows.filter(
378
+ (tr) => tr.style.display !== "none" && tr.offsetParent !== null
379
+ );
380
+
381
+ const headerOffset = "var(--advanced-table-header-height, 44px)";
382
+ visibleRows.forEach((tr, index) => {
383
+ tr.style.top = `calc(${headerOffset} + 2.5em * ${index})`;
384
+ });
385
+ }
386
+
310
387
  hideCloseIcon() {
311
388
  const closeIcon = this.element.querySelector(UP_ARROW_SELECTOR);
312
389
  closeIcon.style.display = "none";
@@ -316,11 +393,9 @@ export default class PbAdvancedTable extends PbEnhancedElement {
316
393
  elements.forEach((elem) => {
317
394
  elem.style.display = "table-row";
318
395
  elem.classList.add("is-visible");
319
- const childRowsAll = this.element
320
- .closest("table")
321
- .querySelectorAll(
322
- `[data-advanced-table-content^="${elem.dataset.advancedTableContent}-"]`
323
- );
396
+ const childRowsAll = this.table.querySelectorAll(
397
+ `[data-advanced-table-content^="${elem.dataset.advancedTableContent}-"]`
398
+ );
324
399
 
325
400
  childRowsAll.forEach((childRow) => {
326
401
  const dataContent = childRow.dataset.advancedTableContent;
@@ -382,8 +457,7 @@ export default class PbAdvancedTable extends PbEnhancedElement {
382
457
  const currentDepth = parseInt(elem.dataset.rowDepth);
383
458
  if (childrenArray.length > currentDepth) {
384
459
  // Find the child rows corresponding to this parent row
385
- const childRows = this.element
386
- .closest("table")
460
+ const childRows = this.table
387
461
  .querySelectorAll(
388
462
  `[data-advanced-table-content^="${elem.dataset.advancedTableContent}-"]`
389
463
  );
@@ -401,28 +475,39 @@ export default class PbAdvancedTable extends PbEnhancedElement {
401
475
 
402
476
  const isVisible = elements[0].classList.contains("is-visible");
403
477
 
404
- isVisible ? this.hideElement(elements) : this.showElement(elements);
405
- isVisible ? this.displayDownArrow() : this.displayUpArrow();
478
+ const isExpanded = toggleVisibility({
479
+ isVisible,
480
+ onHide: () => this.hideElement(elements),
481
+ onShow: () => this.showElement(elements),
482
+ });
483
+
484
+ isExpanded ? this.displayUpArrow() : this.displayDownArrow();
406
485
 
407
486
  const row = this.element.closest("tr");
408
487
  if (row) {
409
- row.classList.toggle("bg-silver", !isVisible);
410
- row.classList.toggle("pb-bg-row-white", isVisible);
488
+ row.classList.toggle("bg-silver", isExpanded);
489
+ row.classList.toggle("pb-bg-row-white", !isExpanded);
411
490
  }
412
491
 
413
492
  this.addBorderRadiusOnLastVisibleRow();
414
493
  }
415
494
 
416
495
  displayDownArrow() {
417
- this.element.querySelector(DOWN_ARROW_SELECTOR).style.display =
418
- "inline-block";
419
- this.element.querySelector(UP_ARROW_SELECTOR).style.display = "none";
496
+ setArrowVisibility({
497
+ rootElement: this.element,
498
+ downSelector: DOWN_ARROW_SELECTOR,
499
+ upSelector: UP_ARROW_SELECTOR,
500
+ showDownArrow: true,
501
+ });
420
502
  }
421
503
 
422
504
  displayUpArrow() {
423
- this.element.querySelector(UP_ARROW_SELECTOR).style.display =
424
- "inline-block";
425
- this.element.querySelector(DOWN_ARROW_SELECTOR).style.display = "none";
505
+ setArrowVisibility({
506
+ rootElement: this.element,
507
+ downSelector: DOWN_ARROW_SELECTOR,
508
+ upSelector: UP_ARROW_SELECTOR,
509
+ showDownArrow: false,
510
+ });
426
511
  }
427
512
 
428
513
  static handleToggleAllHeaders(element) {
@@ -500,3 +585,19 @@ window.expandAllRows = (element) => {
500
585
  window.expandAllSubRows = (element, rowDepth) => {
501
586
  PbAdvancedTable.handleToggleAllSubRows(element, rowDepth);
502
587
  };
588
+
589
+ // Fix header height and pinned row sticky tops on load (header wrap + collapsed rows)
590
+ function updateAllAdvancedTableStickyHeights() {
591
+ document.querySelectorAll(".pb_advanced_table").forEach((wrapper) => {
592
+ PbAdvancedTable.updateStickyHeaderRowHeights(wrapper);
593
+ PbAdvancedTable.updatePinnedRowsStickyTops(wrapper);
594
+ });
595
+ }
596
+
597
+ if (typeof document !== "undefined") {
598
+ if (document.readyState === "loading") {
599
+ document.addEventListener("DOMContentLoaded", updateAllAdvancedTableStickyHeights);
600
+ } else {
601
+ updateAllAdvancedTableStickyHeights();
602
+ }
603
+ }
@@ -1,3 +1,9 @@
1
+ // Acts as outer “card frame” on the advanced-table wrapper (table-card from Table) is included from `_advanced_table.scss` only when `:not(.advanced-table-no-table-container)`/container: false is not present.
2
+ @mixin advanced-table-sticky-wrapper-frame($border-color) {
3
+ border-radius: 4px;
4
+ box-shadow: 1px 0 0 0px $border-color, -1px 0 0 0px $border-color;
5
+ }
6
+
1
7
  @mixin advanced-table-sticky-mixin(
2
8
  $border-color,
3
9
  $bg-main,
@@ -5,8 +11,6 @@
5
11
  $highlight: #E5EEFA,
6
12
  $highlight-dark: #202850,
7
13
  ) {
8
- border-radius: 4px;
9
- box-shadow: 1px 0 0 0px $border-color, -1px 0 0 0px $border-color;
10
14
  display: block;
11
15
  [class^="pb_table"].table-sm.table-card thead tr th:first-child,
12
16
  [class^="pb_table"].table-sm:not(.no-hover).table-card
@@ -1,5 +1,22 @@
1
- <%= pb_content_tag(:tbody) do %>
2
- <% object.table_data.each do |row| %>
3
- <%= render_row_and_children(row, object.column_definitions, 0, false) %>
4
- <% end %>
1
+ <% table_data = object.table_data || [] %>
2
+ <% if object.has_pinned_rows? %>
3
+ <%= pb_content_tag(:tbody, class: "pinned-rows-tbody") do %>
4
+ <% next_index = 0 %>
5
+ <% object.pinned_root_rows.each do |root_info| %>
6
+ <% row_output, next_index = object.render_row_and_children(root_info[:row], object.column_definitions, root_info[:depth], root_info[:depth] > 0, root_info[:ancestor_ids] || [], root_info[:ancestor_ids]&.first, immediate_parent_row_id: object.row_id_for(root_info[:parent_row]), is_pinned_row: true, pinned_index: next_index, initial_table_data_attributes: root_info[:depth].to_i > 0 ? object.pinned_root_initial_data_attributes(root_info) : nil) %>
7
+ <%= row_output %>
8
+ <% end %>
9
+ <% end %>
10
+ <%= pb_content_tag(:tbody) do %>
11
+ <% table_data.each do |row| %>
12
+ <% result = object.render_row_and_children(row, object.column_definitions, 0, false, skip_pinned_ids: object.pinned_ids_set) %>
13
+ <%= result.is_a?(Array) ? result[0] : result %>
14
+ <% end %>
15
+ <% end %>
16
+ <% else %>
17
+ <%= pb_content_tag(:tbody) do %>
18
+ <% table_data.each do |row| %>
19
+ <%= object.render_row_and_children(row, object.column_definitions, 0, false) %>
20
+ <% end %>
21
+ <% end %>
5
22
  <% end %>
@@ -27,6 +27,8 @@ module Playbook
27
27
  default: []
28
28
  prop :inline_row_loading, type: Playbook::Props::Boolean,
29
29
  default: false
30
+ prop :pinned_rows, type: Playbook::Props::HashProp,
31
+ default: {}
30
32
 
31
33
  def flatten_columns(columns)
32
34
  columns.flat_map do |col|
@@ -42,14 +44,21 @@ module Playbook
42
44
  end.compact
43
45
  end
44
46
 
45
- def render_row_and_children(row, column_definitions, current_depth, first_parent_child, ancestor_ids = [], top_parent_id = nil, additional_classes: "", table_data_attributes: {}, immediate_parent_row_id: nil)
47
+ def render_row_and_children(row, column_definitions, current_depth, first_parent_child, ancestor_ids = [], top_parent_id = nil, additional_classes: "", table_data_attributes: {}, immediate_parent_row_id: nil, is_pinned_row: false, pinned_index: nil, skip_pinned_ids: nil, initial_table_data_attributes: nil)
48
+ if skip_pinned_ids && row_id_for(row) && skip_pinned_ids.include?(row_id_for(row).to_s)
49
+ return is_pinned_row ? [ActiveSupport::SafeBuffer.new, pinned_index] : ActiveSupport::SafeBuffer.new
50
+ end
51
+
46
52
  top_parent_id ||= row.object_id
47
53
  new_ancestor_ids = ancestor_ids + [row.object_id]
48
- leaf_columns = flatten_columns(column_definitions)
54
+ leaf_columns = flatten_columns(column_definitions || [])
49
55
 
50
56
  output = ActiveSupport::SafeBuffer.new
51
- is_first_child_of_subrow = current_depth.positive? && first_parent_child && subrow_headers[current_depth - 1].present?
52
- last_row = subrow_headers.length == current_depth
57
+ subrow_headers_arr = subrow_headers || []
58
+ is_first_child_of_subrow = current_depth.positive? && first_parent_child && subrow_headers_arr[current_depth - 1].present?
59
+ last_row = subrow_headers_arr.length == current_depth
60
+
61
+ next_pinned_index = pinned_index
53
62
 
54
63
  subrow_ancestor_ids = ancestor_ids + ["#{row.object_id}sr"]
55
64
  subrow_data_attributes = {
@@ -58,7 +67,16 @@ module Playbook
58
67
  row_parent: "#{table_id}_#{ancestor_ids.last}",
59
68
  }
60
69
  # Subrow header if applicable
61
- output << pb_rails("advanced_table/table_subrow_header", props: { row: row, column_definitions: leaf_columns, depth: current_depth, subrow_header: subrow_headers[current_depth - 1], collapsible_trail: collapsible_trail, classname: "toggle-content", responsive: responsive, subrow_data_attributes: subrow_data_attributes, last_row: last_row, immediate_parent_row_id: immediate_parent_row_id }) if is_first_child_of_subrow && enable_toggle_expansion == "all"
70
+ if is_first_child_of_subrow && enable_toggle_expansion == "all"
71
+ subrow_props = { row: row, column_definitions: leaf_columns, depth: current_depth, subrow_header: subrow_headers_arr[current_depth - 1], collapsible_trail: collapsible_trail, classname: "toggle-content", responsive: responsive, subrow_data_attributes: subrow_data_attributes, last_row: last_row, immediate_parent_row_id: immediate_parent_row_id }
72
+ if is_pinned_row && next_pinned_index
73
+ subrow_props[:is_pinned_row] = true
74
+ subrow_props[:pinned_index] = next_pinned_index
75
+ subrow_props[:html_options] = { style: build_pinned_row_style(next_pinned_index, background: "var(--pb_table_sticky_bg, #f5f5f5)") }
76
+ next_pinned_index += 1
77
+ end
78
+ output << pb_rails("advanced_table/table_subrow_header", props: subrow_props)
79
+ end
62
80
 
63
81
  current_data_attributes = if current_depth.zero?
64
82
  {
@@ -67,11 +85,19 @@ module Playbook
67
85
  row_parent: nil,
68
86
  }
69
87
  else
70
- table_data_attributes
88
+ initial_table_data_attributes || table_data_attributes
71
89
  end
72
90
 
73
91
  # Additional class and data attributes needed for toggle logic
74
- output << pb_rails("advanced_table/table_row", props: { table_id: table_id, row: row, column_definitions: leaf_columns, depth: current_depth, collapsible_trail: collapsible_trail, classname: additional_classes, table_data_attributes: current_data_attributes, responsive: responsive, loading: loading, selectable_rows: selectable_rows, row_id: row[:id], enable_toggle_expansion: enable_toggle_expansion, row_styling: row_styling, last_row: last_row, immediate_parent_row_id: immediate_parent_row_id, inline_row_loading: inline_row_loading })
92
+ row_props = { table_id: table_id, row: row, column_definitions: leaf_columns, depth: current_depth, collapsible_trail: collapsible_trail, classname: additional_classes, table_data_attributes: current_data_attributes, responsive: responsive, loading: loading, selectable_rows: selectable_rows, row_id: row[:id], enable_toggle_expansion: enable_toggle_expansion, row_styling: row_styling, last_row: last_row, immediate_parent_row_id: immediate_parent_row_id, inline_row_loading: inline_row_loading }
93
+ if is_pinned_row && next_pinned_index
94
+ row_props[:is_pinned_row] = true
95
+ row_props[:pinned_index] = next_pinned_index
96
+ row_bg = (row_styling || []).find { |s| s[:row_id].to_s == row_id_for(row).to_s }&.[](:background_color) || "white"
97
+ row_props[:html_options] = { style: build_pinned_row_style(next_pinned_index, background: row_bg) }
98
+ next_pinned_index += 1
99
+ end
100
+ output << pb_rails("advanced_table/table_row", props: row_props)
75
101
 
76
102
  # Render inline loading row when inline_row_loading is enabled and row has empty children
77
103
  if inline_row_loading
@@ -103,11 +129,21 @@ module Playbook
103
129
  advanced_table_content: data_content,
104
130
  }
105
131
 
106
- output << render_row_and_children(child_row, column_definitions, current_depth + 1, is_first_child, new_ancestor_ids, top_parent_id, additional_classes: "toggle-content", table_data_attributes: child_data_attributes, immediate_parent_row_id: row[:id])
132
+ child_opts = { additional_classes: "toggle-content", table_data_attributes: child_data_attributes, immediate_parent_row_id: row[:id] }
133
+ child_opts[:is_pinned_row] = is_pinned_row
134
+ child_opts[:pinned_index] = next_pinned_index if is_pinned_row
135
+ child_opts[:skip_pinned_ids] = skip_pinned_ids if skip_pinned_ids
136
+
137
+ child_output, next_pinned_index = render_row_and_children(child_row, column_definitions, current_depth + 1, is_first_child, new_ancestor_ids, top_parent_id, **child_opts)
138
+ output << child_output
107
139
  end
108
140
  end
109
141
 
110
- output
142
+ if is_pinned_row
143
+ [output, next_pinned_index]
144
+ else
145
+ output
146
+ end
111
147
  end
112
148
 
113
149
  def classname
@@ -142,6 +178,76 @@ module Playbook
142
178
  end
143
179
  end
144
180
 
181
+ def row_id_for(row)
182
+ return nil if row.nil?
183
+
184
+ row[:id] || row["id"]
185
+ end
186
+
187
+ def pinned_top_ids
188
+ return [] if pinned_rows.nil? || !pinned_rows.respond_to?(:[])
189
+
190
+ top = pinned_rows["top"] || pinned_rows[:top]
191
+ Array(top).map(&:to_s)
192
+ end
193
+
194
+ def pinned_ids_set
195
+ return Set.new if pinned_top_ids.blank?
196
+
197
+ set = Set.new
198
+ pinned_root_rows.each do |root|
199
+ collect_row_and_descendant_ids(root[:row], set)
200
+ end
201
+ set
202
+ end
203
+
204
+ def collect_row_and_descendant_ids(row, set)
205
+ id = row_id_for(row)
206
+ set.add(id.to_s) if id
207
+ row_children_for(row)&.each { |child| collect_row_and_descendant_ids(child, set) }
208
+ end
209
+
210
+ def find_row_by_id(data, id, depth: 0, ancestor_ids: [], parent_row: nil)
211
+ id_str = id.to_s
212
+ Array(data).each do |row|
213
+ return { row: row, depth: depth, ancestor_ids: ancestor_ids, parent_row: parent_row } if row_id_for(row).to_s == id_str
214
+
215
+ found = find_row_by_id(row_children_for(row), id_str, depth: depth + 1, ancestor_ids: ancestor_ids + [row.object_id], parent_row: row)
216
+ return found if found
217
+ end
218
+ nil
219
+ end
220
+
221
+ def pinned_root_rows
222
+ return [] if pinned_top_ids.blank?
223
+
224
+ pinned_top_ids.filter_map { |id| find_row_by_id(table_data, id) }
225
+ end
226
+
227
+ def has_pinned_rows?
228
+ pinned_root_rows.any?
229
+ end
230
+
231
+ # Build inline style for sticky pinned row (matches React). Pass via html_options so the tr gets the attribute.
232
+ def build_pinned_row_style(pinned_index, background: "white")
233
+ header_offset = "var(--advanced-table-header-height, 44px)"
234
+ row_offset = "calc(2.5em * #{pinned_index})"
235
+ "position: sticky; top: calc(#{header_offset} + #{row_offset}); z-index: 3; background: #{background};"
236
+ end
237
+
238
+ def pinned_root_initial_data_attributes(root_info)
239
+ return {} if root_info[:depth].to_i.zero?
240
+
241
+ anc = root_info[:ancestor_ids] || []
242
+ content = (anc + [root_info[:row].object_id]).join("-")
243
+ {
244
+ top_parent: "#{table_id}_#{anc.first}",
245
+ row_depth: root_info[:depth],
246
+ row_parent: "#{table_id}_#{anc.last}",
247
+ advanced_table_content: content,
248
+ }
249
+ end
250
+
145
251
  def cell_accessors_length(col_defs)
146
252
  first_col = col_defs.first
147
253
  return 0 unless first_col
@@ -3,9 +3,11 @@
3
3
  button_color = row_style&.[](:expand_button_color)
4
4
  bg_color = row_style&.[](:background_color)
5
5
  font_color = row_style&.[](:font_color)
6
+ tr_options = (object.html_options || {}).stringify_keys
7
+ tr_options["class"] = [tr_options["class"], object.classname].reject(&:blank?).join(" ")
6
8
  %>
7
9
 
8
- <%= pb_content_tag(:tr) do %>
10
+ <%= pb_content_tag(:tr, tr_options) do %>
9
11
  <% has_separate_checkbox = object.selectable_rows && object.enable_toggle_expansion == "none" %>
10
12
  <% if has_separate_checkbox %>
11
13
  <%= object.render_checkbox_cell %>
@@ -35,13 +35,24 @@ module Playbook
35
35
  default: ""
36
36
  prop :inline_row_loading, type: Playbook::Props::Boolean,
37
37
  default: false
38
+ prop :is_pinned_row, type: Playbook::Props::Boolean,
39
+ default: false
40
+ prop :pinned_index, type: Playbook::Props::Numeric,
41
+ default: nil
42
+ prop :html_options, type: Playbook::Props::HashProp,
43
+ default: {}
44
+ prop :classname, type: Playbook::Props::String,
45
+ default: ""
38
46
 
39
47
  def data
40
48
  Hash(prop(:data)).merge(table_data_attributes)
41
49
  end
42
50
 
43
51
  def classname
44
- generate_classname("pb_table_tr", "pb-bg-row-white", subrow_depth_classname, separator: " ")
52
+ classes = ["pb_table_tr", "pb-bg-row-white", subrow_depth_classname]
53
+ classes << "pinned-row" if is_pinned_row
54
+ classes.reject!(&:blank?)
55
+ generate_classname(*classes, separator: " ")
45
56
  end
46
57
 
47
58
  def td_classname(column, index)
@@ -1,4 +1,7 @@
1
- <%= pb_content_tag(:tr) do %>
1
+ <% tr_options = (object.html_options || {}).stringify_keys %>
2
+ <% tr_options["class"] = [tr_options["class"], object.classname].reject(&:blank?).join(" ") %>
3
+
4
+ <%= pb_content_tag(:tr, tr_options) do %>
2
5
  <% object.column_definitions.each_with_index do |column, index| %>
3
6
  <%= pb_rails("table/table_cell", props: { classname: object.td_classname(index) }) do %>
4
7
  <%= pb_rails("flex", props:{ align: "center", justify: "start" }) do %>
@@ -19,13 +19,21 @@ module Playbook
19
19
  prop :responsive, type: Playbook::Props::Enum,
20
20
  values: %w[none scroll],
21
21
  default: "scroll"
22
+ prop :is_pinned_row, type: Playbook::Props::Boolean,
23
+ default: false
24
+ prop :pinned_index, type: Playbook::Props::Numeric,
25
+ default: nil
26
+ prop :html_options, type: Playbook::Props::HashProp,
27
+ default: {}
22
28
 
23
29
  def data
24
30
  Hash(prop(:data)).merge(subrow_data_attributes)
25
31
  end
26
32
 
27
33
  def classname
28
- generate_classname("pb_table_tr", "bg-silver", "pb_subrow_header", subrow_depth_classname, separator: " ")
34
+ classes = ["pb_table_tr", "bg-silver", "pb_subrow_header", subrow_depth_classname]
35
+ classes << "pinned-row" if is_pinned_row
36
+ generate_classname(*classes, separator: " ")
29
37
  end
30
38
 
31
39
  def td_classname(index)
@@ -57,7 +57,7 @@ $pb_button_border_width: 0px;
57
57
  }
58
58
 
59
59
  .loading-icon {
60
- position: absolute;
60
+ position: static;
61
61
  display: none;
62
62
  }
63
63
  .pb_button_content {
@@ -158,10 +158,15 @@ $pb_button_border_width: 0px;
158
158
  // Loading =====================
159
159
  @mixin pb_button_loading($loading: false) {
160
160
  @if $loading == true {
161
+ display: inline-grid;
162
+ place-items: center;
163
+
161
164
  .loading-icon {
165
+ grid-area: 1 / 1;
162
166
  display: block;
163
167
  }
164
168
  .pb_button_content {
169
+ grid-area: 1 / 1;
165
170
  visibility: hidden;
166
171
  }
167
172
  }
@@ -0,0 +1,19 @@
1
+ This button is used many times for mobile or other things like cards and sidebars.
2
+
3
+ ### Responsive `display` and `full_width`
4
+
5
+ `full_width` applies block styling that includes `display: flex` on the **same element** as the button. The **`display` global prop** also sets `display` (via utility classes, often with `!important`).
6
+
7
+ Putting **both** on one button means **two systems control `display` on one node**, which can cause wrong visibility (e.g. both a header and a full-width mobile button showing) or confusing cascade behavior.
8
+
9
+ **Recommended:** Put responsive `display` on a **parent** (e.g. `Flex`, `Card`, or a plain wrapper) and keep `full_width` only on the `Button` inside. The wrapper handles show/hide by breakpoint; the button only handles full-width layout.
10
+
11
+ ```erb
12
+ <%= pb_rails("flex", props: {
13
+ display: { xs: "flex", default: "none" },
14
+ orientation: "column",
15
+ width: "100%",
16
+ }) do %>
17
+ <%= pb_rails("button", props: { full_width: true, text: "Add" }) %>
18
+ <% end %>
19
+ ```
@@ -0,0 +1,23 @@
1
+ This button is used many times for mobile or other things like cards and sidebars.
2
+
3
+ ### Responsive `display` and `full_width`
4
+
5
+ `full_width` applies block styling that includes `display: flex` on the **same element** as the button. The **`display` global prop** also sets `display` (via utility classes, often with `!important`).
6
+
7
+ Putting **both** on one button means **two systems control `display` on one node**, which can cause wrong visibility (e.g. both a header and a full-width mobile button showing) or confusing cascade behavior.
8
+
9
+ **Recommended:** Put responsive `display` on a **parent** (e.g. `Flex`, `Card`, or a plain wrapper) and keep `fullWidth` only on the `Button` inside. The wrapper handles show/hide by breakpoint; the button only handles full-width layout.
10
+
11
+ ```jsx
12
+ import { Flex, Button } from "playbook-ui"
13
+
14
+ const Example = () => (
15
+ <Flex
16
+ display={{ xs: "flex", default: "none" }}
17
+ orientation="column"
18
+ width="100%"
19
+ >
20
+ <Button fullWidth text="Add" />
21
+ </Flex>
22
+ )
23
+ ```
@@ -43,6 +43,11 @@ $pb_button_styles: (
43
43
  @include pb_circle_icon_button;
44
44
  }
45
45
  }
46
+
47
+ .pb_button_kit.pb_button_loading svg.loading-icon {
48
+ position: absolute;
49
+ }
50
+
46
51
  :first-child {
47
52
  &.pb_button_kit.pb_button_link {
48
53
  @include pb_circle_icon_button_active;