playbook_ui 16.3.0.pre.alpha.play285814889 → 16.3.0.pre.alpha.railspinnedrows14948

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/Hooks/useTableState.ts +5 -2
  3. data/app/pb_kits/playbook/pb_advanced_table/Utilities/RowModelUtils.ts +100 -0
  4. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.scss +46 -1
  5. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.tsx +4 -1
  6. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.html.erb +2 -2
  7. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.rb +2 -0
  8. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx +35 -1
  9. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows_rails.html.erb +57 -0
  10. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows_rails.md +7 -0
  11. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_sort_parent_only.jsx +175 -0
  12. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_sort_parent_only.md +5 -0
  13. data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +2 -0
  14. data/app/pb_kits/playbook/pb_advanced_table/docs/index.js +2 -1
  15. data/app/pb_kits/playbook/pb_advanced_table/index.js +82 -0
  16. data/app/pb_kits/playbook/pb_advanced_table/table_body.html.erb +21 -4
  17. data/app/pb_kits/playbook/pb_advanced_table/table_body.rb +115 -9
  18. data/app/pb_kits/playbook/pb_advanced_table/table_row.html.erb +3 -1
  19. data/app/pb_kits/playbook/pb_advanced_table/table_row.rb +12 -1
  20. data/app/pb_kits/playbook/pb_advanced_table/table_subrow_header.html.erb +4 -1
  21. data/app/pb_kits/playbook/pb_advanced_table/table_subrow_header.rb +9 -1
  22. data/app/pb_kits/playbook/pb_date_picker/date_picker_helper.ts +3 -1
  23. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_custom_event_type.html.erb +224 -0
  24. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_custom_event_type.md +7 -0
  25. data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +1 -0
  26. data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +4 -1
  27. data/app/pb_kits/playbook/pb_dropdown/index.js +161 -0
  28. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/_fixed_confirmation_toast.scss +4 -0
  29. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/_fixed_confirmation_toast.tsx +3 -0
  30. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_nav_margin.html.erb +46 -0
  31. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_nav_margin.jsx +42 -0
  32. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_nav_margin_rails.md +1 -0
  33. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_nav_margin_react.md +1 -0
  34. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/example.yml +2 -0
  35. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/index.js +2 -1
  36. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/fixed_confirmation_toast.rb +7 -1
  37. data/dist/chunks/{_pb_line_graph-BI5wY8Wj.js → _pb_line_graph-D6s5rymw.js} +1 -1
  38. data/dist/chunks/{_typeahead-8CvXJGlb.js → _typeahead-Bh0RF1X-.js} +1 -1
  39. data/dist/chunks/{globalProps-Bn1WUHLp.js → globalProps-Ds_6HBhX.js} +1 -1
  40. data/dist/chunks/{lib-qwWYiGtH.js → lib-BaO72ugL.js} +1 -1
  41. data/dist/chunks/vendor.js +3 -3
  42. data/dist/menu.yml +1 -1
  43. data/dist/playbook-rails-react-bindings.js +1 -1
  44. data/dist/playbook-rails.js +1 -1
  45. data/dist/playbook.css +1 -1
  46. data/lib/playbook/version.rb +1 -1
  47. metadata +17 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02d49483b3956da77f73aacb7718f16ebd2b6b8782c44e80815f22be2ffcde4e
4
- data.tar.gz: 693802d5534038fe8a607937538cd3b9370627d0613a09353f9c14b4ed6bb721
3
+ metadata.gz: 4bcbf435e4eba2bfaadf18bb80b6f03e9aeaa464fdca4521d1943d70f2b58506
4
+ data.tar.gz: f59ca24bf67f770d0e13ed0ca8c4770c41001a10dd029de232d1437e4b96d96c
5
5
  SHA512:
6
- metadata.gz: 9878ce5bb1f4e16cd4dad4389e981eacc4ce7aab095d9c39fa0f3b9411768b0732ff6e96b12d35eaf67d0f34f0fdf85e71b5b063d406c3e44bd9f1d6d2389625
7
- data.tar.gz: 4bc358a3ec5ede3933e21e49627d59353a776ed6ff602da16d0c2c509df8297486c56c7b10470504f37040d61e04ca8635e597181cab5eb9c9386635ebe3781b
6
+ metadata.gz: 04cb664168bba64dfcd8018edaf8e114beb74c1e1d231765b3ad154dcd2d1e5cb80955b81f8c6ed523e48fa6059a639c9b0ecd4ab5e42da0fa267e7a01ae8d68
7
+ data.tar.gz: 3b2b50c314d40092c160fc971b1481ecf7f5e9dcbe918b9c126849e498d31d5953c1f270500f449f687507259e0bf631f0571f0976a59101e596eceecb4c5deb
@@ -12,6 +12,7 @@ import {
12
12
  import { GenericObject } from "../../types";
13
13
  import { createColumnHelper } from "@tanstack/react-table";
14
14
  import { createCellFunction } from "../Utilities/CellRendererUtils";
15
+ import { getParentOnlySortedRowModel } from "../Utilities/RowModelUtils";
15
16
 
16
17
  interface UseTableStateProps {
17
18
  tableData: GenericObject[];
@@ -36,6 +37,7 @@ interface UseTableStateProps {
36
37
  columnVisibilityControl?: GenericObject;
37
38
  rowStyling?: GenericObject;
38
39
  inlineRowLoading?: boolean;
40
+ sortParentOnly?: boolean;
39
41
  }
40
42
 
41
43
  export function useTableState({
@@ -55,7 +57,8 @@ export function useTableState({
55
57
  columnVisibilityControl,
56
58
  pinnedRows,
57
59
  rowStyling,
58
- inlineRowLoading = false
60
+ inlineRowLoading = false,
61
+ sortParentOnly = false
59
62
  }: UseTableStateProps) {
60
63
 
61
64
  // Create a local state for expanded and setExpanded if expandedControl not used
@@ -190,7 +193,7 @@ export function useTableState({
190
193
  getSubRows: (row: GenericObject) => row.children,
191
194
  getCoreRowModel: getCoreRowModel(),
192
195
  getExpandedRowModel: getExpandedRowModel(),
193
- getSortedRowModel: getSortedRowModel(),
196
+ getSortedRowModel: sortParentOnly ? getParentOnlySortedRowModel() : getSortedRowModel(),
194
197
  enableSortingRemoval: enableSortingRemoval,
195
198
  sortDescFirst: true,
196
199
  onRowSelectionChange: setRowSelection,
@@ -0,0 +1,100 @@
1
+ // Returns a row model getter that sorts only depth-0 (parent) rows so children and grandchild rows keep their original order under each parent.
2
+
3
+ import type { Table, Row, RowModel, RowData } from "@tanstack/react-table";
4
+
5
+ export function getParentOnlySortedRowModel<TData extends RowData>(): (
6
+ table: Table<TData>
7
+ ) => () => RowModel<TData> {
8
+ return (table) => () => {
9
+ const sortingState = table.getState().sorting;
10
+ const rowModel = table.getPreSortedRowModel();
11
+
12
+ if (!rowModel.rows.length || !sortingState?.length) {
13
+ return rowModel;
14
+ }
15
+
16
+ const sortedFlatRows: Row<TData>[] = [];
17
+ const availableSorting = sortingState.filter((sort) =>
18
+ table.getColumn(sort.id)?.getCanSort()
19
+ );
20
+
21
+ const columnInfoById: Record<
22
+ string,
23
+ {
24
+ sortUndefined?: false | -1 | 1 | "first" | "last";
25
+ invertSorting?: boolean;
26
+ sortingFn: (rowA: Row<TData>, rowB: Row<TData>, columnId: string) => number;
27
+ }
28
+ > = {};
29
+
30
+ availableSorting.forEach((sortEntry) => {
31
+ const column = table.getColumn(sortEntry.id);
32
+ if (!column) return;
33
+ columnInfoById[sortEntry.id] = {
34
+ sortUndefined: column.columnDef.sortUndefined,
35
+ invertSorting: column.columnDef.invertSorting,
36
+ sortingFn: column.getSortingFn(),
37
+ };
38
+ });
39
+
40
+ const parentRows = rowModel.rows.map((row) => ({ ...row }));
41
+ parentRows.sort((rowA, rowB) => {
42
+ for (let i = 0; i < availableSorting.length; i += 1) {
43
+ const sortEntry = availableSorting[i]!;
44
+ const columnInfo = columnInfoById[sortEntry.id]!;
45
+ const sortUndefined = columnInfo.sortUndefined;
46
+ const isDesc = sortEntry?.desc ?? false;
47
+ let sortInt = 0;
48
+
49
+ if (sortUndefined) {
50
+ const aValue = rowA.getValue(sortEntry.id);
51
+ const bValue = rowB.getValue(sortEntry.id);
52
+ const aUndefined = aValue === undefined;
53
+ const bUndefined = bValue === undefined;
54
+ if (aUndefined || bUndefined) {
55
+ if (sortUndefined === "first") return aUndefined ? -1 : 1;
56
+ if (sortUndefined === "last") return aUndefined ? 1 : -1;
57
+ sortInt =
58
+ aUndefined && bUndefined
59
+ ? 0
60
+ : aUndefined
61
+ ? sortUndefined
62
+ : -sortUndefined;
63
+ }
64
+ }
65
+
66
+ if (sortInt === 0) {
67
+ sortInt = columnInfo.sortingFn(rowA, rowB, sortEntry.id);
68
+ }
69
+
70
+ if (sortInt !== 0) {
71
+ if (isDesc) sortInt *= -1;
72
+ if (columnInfo.invertSorting) sortInt *= -1;
73
+ return sortInt;
74
+ }
75
+ }
76
+ return rowA.index - rowB.index;
77
+ });
78
+
79
+ function flattenRowsInOrder(rows: Row<TData>[]): void {
80
+ rows.forEach((row) => {
81
+ sortedFlatRows.push(row);
82
+ if (row.subRows?.length) {
83
+ flattenRowsInOrder(row.subRows);
84
+ }
85
+ });
86
+ }
87
+ flattenRowsInOrder(parentRows);
88
+
89
+ const rowsById: Record<string, Row<TData>> = {};
90
+ sortedFlatRows.forEach((row) => {
91
+ rowsById[row.id] = row;
92
+ });
93
+
94
+ return {
95
+ rows: parentRows,
96
+ flatRows: sortedFlatRows,
97
+ rowsById,
98
+ };
99
+ };
100
+ }
@@ -63,6 +63,51 @@
63
63
  width: 100%;
64
64
  }
65
65
 
66
+ // Override pb_table endcap: first and last column use same padding as rest of row
67
+ // First column
68
+ .pb_table.table-sm tbody tr td:first-child,
69
+ .pb_table.table-sm tbody tr .pb_table_td:first-child,
70
+ .pb_table.table-sm tbody .pb_table_tr td:first-child,
71
+ .pb_table.table-sm tbody .pb_table_tr .pb_table_td:first-child,
72
+ .pb_table.table-sm .pb_table_tbody tr td:first-child,
73
+ .pb_table.table-sm .pb_table_tbody tr .pb_table_td:first-child,
74
+ .pb_table.table-sm .pb_table_tbody .pb_table_tr td:first-child,
75
+ .pb_table.table-sm .pb_table_tbody .pb_table_tr .pb_table_td:first-child {
76
+ padding-left: $space-xs;
77
+ }
78
+ .pb_table.table-md tbody tr td:first-child,
79
+ .pb_table.table-md tbody tr .pb_table_td:first-child,
80
+ .pb_table.table-md tbody .pb_table_tr td:first-child,
81
+ .pb_table.table-md tbody .pb_table_tr .pb_table_td:first-child,
82
+ .pb_table.table-md .pb_table_tbody tr td:first-child,
83
+ .pb_table.table-md .pb_table_tbody tr .pb_table_td:first-child,
84
+ .pb_table.table-md .pb_table_tbody .pb_table_tr td:first-child,
85
+ .pb_table.table-md .pb_table_tbody .pb_table_tr .pb_table_td:first-child {
86
+ padding-left: $space-sm;
87
+ }
88
+
89
+ // Last column
90
+ .pb_table.table-sm tbody tr td:last-child,
91
+ .pb_table.table-sm tbody tr .pb_table_td:last-child,
92
+ .pb_table.table-sm tbody .pb_table_tr td:last-child,
93
+ .pb_table.table-sm tbody .pb_table_tr .pb_table_td:last-child,
94
+ .pb_table.table-sm .pb_table_tbody tr td:last-child,
95
+ .pb_table.table-sm .pb_table_tbody tr .pb_table_td:last-child,
96
+ .pb_table.table-sm .pb_table_tbody .pb_table_tr td:last-child,
97
+ .pb_table.table-sm .pb_table_tbody .pb_table_tr .pb_table_td:last-child {
98
+ padding-right: $space-xs;
99
+ }
100
+ .pb_table.table-md tbody tr td:last-child,
101
+ .pb_table.table-md tbody tr .pb_table_td:last-child,
102
+ .pb_table.table-md tbody .pb_table_tr td:last-child,
103
+ .pb_table.table-md tbody .pb_table_tr .pb_table_td:last-child,
104
+ .pb_table.table-md .pb_table_tbody tr td:last-child,
105
+ .pb_table.table-md .pb_table_tbody tr .pb_table_td:last-child,
106
+ .pb_table.table-md .pb_table_tbody .pb_table_tr td:last-child,
107
+ .pb_table.table-md .pb_table_tbody .pb_table_tr .pb_table_td:last-child {
108
+ padding-right: $space-sm;
109
+ }
110
+
66
111
  // Virtualized Table and Rows for Infinite Scroll
67
112
  scrollbar-gutter: stable right-edges;
68
113
  .virtualized-header-row-header {
@@ -720,7 +765,7 @@
720
765
  }
721
766
  }
722
767
 
723
- // Row Pinning - additional inline styles in RegularTableView.tsx
768
+ // Row Pinning - React uses inline style; Rails passes same style via html_options from table_body
724
769
  .pinned-row {
725
770
  box-shadow: 0 4px 10px 0 rgba($shadow, 0.16) !important;
726
771
  }
@@ -66,6 +66,7 @@ type AdvancedTableProps = {
66
66
  showActionsBar?: boolean,
67
67
  persistToggleExpansionButton?: boolean,
68
68
  sortControl?: GenericObject
69
+ sortParentOnly?: boolean
69
70
  tableData: GenericObject[]
70
71
  tableOptions?: GenericObject
71
72
  tableProps?: GenericObject
@@ -114,6 +115,7 @@ const AdvancedTable = (props: AdvancedTableProps) => {
114
115
  selectableRows,
115
116
  persistToggleExpansionButton = false,
116
117
  sortControl,
118
+ sortParentOnly = false,
117
119
  stickyLeftColumn,
118
120
  tableData,
119
121
  tableOptions,
@@ -159,7 +161,8 @@ const AdvancedTable = (props: AdvancedTableProps) => {
159
161
  columnVisibilityControl,
160
162
  pinnedRows,
161
163
  rowStyling,
162
- inlineRowLoading
164
+ inlineRowLoading,
165
+ sortParentOnly
163
166
  });
164
167
 
165
168
  // Initialize table actions
@@ -7,12 +7,12 @@
7
7
  }) %>
8
8
  <% end %>
9
9
 
10
- <%= pb_rails("table", props: { size: "sm", data_table: true, number_spacing:"tabular", responsive:"none", dark: dark, classname: object.loading ? "content-loading" : "" }.merge(object.table_props)) do %>
10
+ <%= pb_rails("table", props: { size: "sm", data_table: true, number_spacing:"tabular", responsive:"none", dark: dark, classname: object.loading ? "content-loading" : "" }.merge(object.table_props || {})) do %>
11
11
  <% if content.present? %>
12
12
  <% content.presence %>
13
13
  <% else %>
14
14
  <%= pb_rails("advanced_table/table_header", props: { table_id: object.id, column_definitions: object.column_definitions, enable_toggle_expansion: object.enable_toggle_expansion, responsive: object.responsive, loading: object.loading, selectable_rows: object.selectable_rows, show_actions_bar: object.show_actions_bar, inline_row_loading: object.inline_row_loading, persist_toggle_expansion_button: object.persist_toggle_expansion_button, table_data: object.table_data }) %>
15
- <%= pb_rails("advanced_table/table_body", props: { table_id: object.id, table_data: object.table_data, column_definitions: object.column_definitions, responsive: object.responsive, loading: object.loading, selectable_rows: object.selectable_rows, enable_toggle_expansion: object.enable_toggle_expansion, row_styling: object.row_styling, inline_row_loading: object.inline_row_loading }) %>
15
+ <%= pb_rails("advanced_table/table_body", props: { table_id: object.id, table_data: object.table_data, column_definitions: object.column_definitions, responsive: object.responsive, loading: object.loading, selectable_rows: object.selectable_rows, enable_toggle_expansion: object.enable_toggle_expansion, row_styling: object.row_styling, inline_row_loading: object.inline_row_loading, pinned_rows: object.pinned_rows }) %>
16
16
  <% end %>
17
17
  <% end %>
18
18
  <% end %>
@@ -37,6 +37,8 @@ module Playbook
37
37
  default: false
38
38
  prop :persist_toggle_expansion_button, type: Playbook::Props::Boolean,
39
39
  default: false
40
+ prop :pinned_rows, type: Playbook::Props::HashProp,
41
+ default: {}
40
42
 
41
43
  def classname
42
44
  additional_classes = [
@@ -495,7 +495,41 @@ test("sort button exists and sorts column data", () => {
495
495
 
496
496
  const row2 = kit.getElementsByTagName('tr')[2]
497
497
  expect(row2.id).toBe("0-0-0-row")
498
- })
498
+ })
499
+
500
+ test("sortParentOnly sorts only parent rows and keeps children grouped under parent", () => {
501
+ render(
502
+ <AdvancedTable
503
+ columnDefinitions={columnDefinitions}
504
+ data={{ testid: testId }}
505
+ sortParentOnly
506
+ tableData={MOCK_DATA}
507
+ >
508
+ <AdvancedTable.Header enableSorting />
509
+ <AdvancedTable.Body />
510
+ </AdvancedTable>
511
+ )
512
+
513
+ const kit = screen.getByTestId(testId)
514
+ const sortButton = kit.querySelector(".header-sort-button.pb_th_link")
515
+ expect(sortButton).toBeInTheDocument()
516
+
517
+ const tbody = kit.querySelector('tbody')
518
+ const rowsBefore = tbody.getElementsByTagName('tr')
519
+ expect(rowsBefore[0]).toHaveTextContent('2021')
520
+
521
+ sortButton.click()
522
+
523
+ const rowsAfter = tbody.getElementsByTagName('tr')
524
+ expect(rowsAfter[0]).toHaveTextContent('2022')
525
+
526
+ const expandButton = kit.querySelector(".gray-icon.expand-toggle-icon")
527
+ expandButton.click()
528
+
529
+ const rowsExpanded = tbody.getElementsByTagName('tr')
530
+ expect(rowsExpanded.length).toBeGreaterThan(1)
531
+ expect(rowsExpanded[1]).toHaveTextContent('Q1')
532
+ })
499
533
 
500
534
  test("Generates Table.Header default + custom classname", () => {
501
535
  render(
@@ -0,0 +1,57 @@
1
+ <%# Example sort method for demonstration purposes %>
2
+ <% if params["sort"] %>
3
+ <% sort_param = params["sort"].gsub(/_(asc|desc)\z/, "") %>
4
+ <% sort_direction = params["sort"].end_with?("_asc") ? 1 : -1 %>
5
+ <% @table_data_with_id.sort! do |a, b|
6
+ value_a = a[sort_param] || a[sort_param.to_sym]
7
+ value_b = b[sort_param] || b[sort_param.to_sym]
8
+
9
+ value_a = value_a.to_i if value_a.is_a?(String) && value_a.match?(/^\d+$/)
10
+ value_b = value_b.to_i if value_b.is_a?(String) && value_b.match?(/^\d+$/)
11
+
12
+ sort_direction * (value_a <=> value_b)
13
+ end %>
14
+ <% end %>
15
+
16
+ <% column_definitions = [
17
+ {
18
+ accessor: "year",
19
+ label: "Year",
20
+ cellAccessors: ["quarter", "month", "day"],
21
+ sort_menu: [
22
+ { item: "Year", link: "?sort=year_asc#pinned_rows_table", active: params["sort"] == "year_asc", direction: "asc" },
23
+ { item: "Year", link: "?sort=year_desc#pinned_rows_table", active: params["sort"] == "year_desc", direction: "desc" }
24
+ ],
25
+ },
26
+ {
27
+ accessor: "newEnrollments",
28
+ label: "New Enrollments",
29
+ },
30
+ {
31
+ accessor: "scheduledMeetings",
32
+ label: "Scheduled Meetings",
33
+ },
34
+ {
35
+ accessor: "attendanceRate",
36
+ label: "Attendance Rate",
37
+ },
38
+ {
39
+ accessor: "completedClasses",
40
+ label: "Completed Classes",
41
+ },
42
+ {
43
+ accessor: "classCompletionRate",
44
+ label: "Class Completion Rate",
45
+ },
46
+ {
47
+ accessor: "graduatedStudents",
48
+ label: "Graduated Students",
49
+ }
50
+ ] %>
51
+
52
+ <% pinned_rows = { top: ["8"] } %>
53
+
54
+ <%= pb_rails("advanced_table", props: { id: "pinned_rows_table", table_data: @table_data_with_id, column_definitions: column_definitions, max_height: "xs", pinned_rows: pinned_rows, responsive: "none", table_props: { sticky: true }}) do %>
55
+ <%= pb_rails("advanced_table/table_header", props: { table_id: "pinned_rows_table", column_definitions: column_definitions }) %>
56
+ <%= pb_rails("advanced_table/table_body", props: { table_id: "pinned_rows_table", table_data: @table_data_with_id, column_definitions: column_definitions, pinned_rows: pinned_rows }) %>
57
+ <% end %>
@@ -0,0 +1,7 @@
1
+ Use the `pinned_rows` prop to pin specific rows to the top of an Advanced Table. Pinned rows will remain at the top when scrolling through table data and will not change position if sorting is used.
2
+
3
+ **NOTE:**
4
+ - Sticky header required: Pinned rows must be used with `sticky: true` via `table_props` (works with both responsive and non-responsive tables)
5
+ - Row ids required: Each object within the `table_data` array must contain a unique `id` in order to attach an id to all Rows for this to function.
6
+ - `pinned_rows` takes a hash with a `top` key containing an array of row ids, as shown in the code snippet below.
7
+ - For expandable rows, use the parent id in `pinned_rows[:top]`; all its children will automatically be pinned with it. If a child id is passed without the parent being pinned, nothing will be pinned.
@@ -0,0 +1,175 @@
1
+ import React from "react"
2
+ import AdvancedTable from '../../pb_advanced_table/_advanced_table'
3
+ import MOCK_DATA from "./advanced_table_mock_data.json"
4
+
5
+ import Caption from "../../pb_caption/_caption"
6
+
7
+ const sharedColumnDefinitions = [
8
+ {
9
+ accessor: "year",
10
+ label: "Year",
11
+ cellAccessors: ["quarter", "month", "day"],
12
+ },
13
+ {
14
+ accessor: "newEnrollments",
15
+ label: "New Enrollments",
16
+ },
17
+ {
18
+ accessor: "scheduledMeetings",
19
+ label: "Scheduled Meetings",
20
+ },
21
+ {
22
+ accessor: "attendanceRate",
23
+ label: "Attendance Rate",
24
+ },
25
+ {
26
+ accessor: "completedClasses",
27
+ label: "Completed Classes",
28
+ },
29
+ {
30
+ accessor: "classCompletionRate",
31
+ label: "Class Completion Rate",
32
+ },
33
+ {
34
+ accessor: "graduatedStudents",
35
+ label: "Graduated Students",
36
+ },
37
+ ]
38
+
39
+ const sortByColumnDefinitions = [
40
+ {
41
+ accessor: "year",
42
+ label: "Year",
43
+ cellAccessors: ["quarter", "month", "day"],
44
+ },
45
+ {
46
+ accessor: "newEnrollments",
47
+ label: "New Enrollments",
48
+ enableSort: true,
49
+ },
50
+ {
51
+ accessor: "scheduledMeetings",
52
+ label: "Scheduled Meetings",
53
+ },
54
+ {
55
+ accessor: "attendanceRate",
56
+ label: "Attendance Rate",
57
+ enableSort: true,
58
+ },
59
+ {
60
+ accessor: "completedClasses",
61
+ label: "Completed Classes",
62
+ },
63
+ {
64
+ accessor: "classCompletionRate",
65
+ label: "Class Completion Rate",
66
+ },
67
+ {
68
+ accessor: "graduatedStudents",
69
+ label: "Graduated Students",
70
+ },
71
+ ]
72
+
73
+ const sortByColumnMultiDefinitions = [
74
+ {
75
+ accessor: "year",
76
+ label: "Year",
77
+ cellAccessors: ["quarter", "month", "day"],
78
+ },
79
+ {
80
+ label: "Enrollment Data",
81
+ columns: [
82
+ {
83
+ label: "Enrollment Stats",
84
+ columns: [
85
+ {
86
+ accessor: "newEnrollments",
87
+ label: "New Enrollments",
88
+ enableSort: true,
89
+ },
90
+ {
91
+ accessor: "scheduledMeetings",
92
+ label: "Scheduled Meetings",
93
+ },
94
+ ],
95
+ },
96
+ ],
97
+ },
98
+ {
99
+ label: "Performance Data",
100
+ columns: [
101
+ {
102
+ label: "Completion Metrics",
103
+ columns: [
104
+ {
105
+ accessor: "completedClasses",
106
+ label: "Completed Classes",
107
+ enableSort: true,
108
+ },
109
+ {
110
+ accessor: "classCompletionRate",
111
+ label: "Class Completion Rate",
112
+ },
113
+ ],
114
+ },
115
+ {
116
+ label: "Attendance",
117
+ columns: [
118
+ {
119
+ accessor: "attendanceRate",
120
+ label: "Attendance Rate",
121
+ },
122
+ {
123
+ accessor: "scheduledMeetings",
124
+ label: "Scheduled Meetings",
125
+ },
126
+ ],
127
+ },
128
+ ],
129
+ },
130
+ ]
131
+
132
+ const AdvancedTableSortParentOnly = (props) => {
133
+ return (
134
+ <div>
135
+ <Caption text="Enable Sorting (first column) + sortParentOnly" />
136
+ <AdvancedTable
137
+ columnDefinitions={sharedColumnDefinitions}
138
+ sortParentOnly
139
+ tableData={MOCK_DATA}
140
+ {...props}
141
+ >
142
+ <AdvancedTable.Header enableSorting />
143
+ <AdvancedTable.Body />
144
+ </AdvancedTable>
145
+ <Caption marginTop="md"
146
+ text="Sort by column + sortParentOnly"
147
+ />
148
+ <AdvancedTable
149
+ columnDefinitions={sortByColumnDefinitions}
150
+ enableSortingRemoval
151
+ sortParentOnly
152
+ tableData={MOCK_DATA}
153
+ {...props}
154
+ >
155
+ <AdvancedTable.Header />
156
+ <AdvancedTable.Body />
157
+ </AdvancedTable>
158
+ <Caption marginTop="md"
159
+ text="Sort by column (multi-column) + sortParentOnly"
160
+ />
161
+ <AdvancedTable
162
+ columnDefinitions={sortByColumnMultiDefinitions}
163
+ enableSortingRemoval
164
+ sortParentOnly
165
+ tableData={MOCK_DATA}
166
+ {...props}
167
+ >
168
+ <AdvancedTable.Header enableSorting />
169
+ <AdvancedTable.Body />
170
+ </AdvancedTable>
171
+ </div>
172
+ )
173
+ }
174
+
175
+ export default AdvancedTableSortParentOnly
@@ -0,0 +1,5 @@
1
+ The `sortParentOnly` prop is a boolean set to `false` by default. When set to `true`, only parent (depth-0) rows are re-ordered when sorting; children and grandchildren stay grouped under their parent and keep their original order.
2
+
3
+ `sortParentOnly` works with every sorting mode: `enableSorting` on the header, per-column `enableSort: true`, and sortable leaf columns in the multi-header variant. Sort indicators behave as usual.
4
+
5
+ When omitted or `false`, sorting applies to all levels.
@@ -7,6 +7,7 @@ examples:
7
7
  - advanced_table_table_props: Table Props
8
8
  - advanced_table_sticky_header_rails: Sticky Header
9
9
  - advanced_table_table_props_sticky_header: Sticky Header for Responsive Table
10
+ - advanced_table_pinned_rows_rails: Pinned Rows
10
11
  - advanced_table_beta_sort: Enable Sorting
11
12
  - advanced_table_responsive: Responsive Tables
12
13
  - advanced_table_custom_cell_rails: Custom Components for Cells
@@ -39,6 +40,7 @@ examples:
39
40
  - advanced_table_sort_per_column: Enable Sort By Column
40
41
  - advanced_table_sort_per_column_for_multi_column: Enable Sort By Column (Multi-Column)
41
42
  - advanced_table_custom_sort: Custom Sort
43
+ - advanced_table_sort_parent_only: Sort Parent Only
42
44
  - advanced_table_expanded_control: Expanded Control
43
45
  - advanced_table_expand_by_depth: Expand by Depth
44
46
  - advanced_table_subrow_headers: SubRow Headers
@@ -49,4 +49,5 @@ export { default as AdvancedTablePaddingControlPerRow } from './_advanced_table_
49
49
  export { default as AdvancedTableColumnStylingBackground } from './_advanced_table_column_styling_background.jsx'
50
50
  export { default as AdvancedTableColumnStylingBackgroundMulti } from './_advanced_table_column_styling_background_multi.jsx'
51
51
  export { default as AdvancedTableColumnStylingBackgroundCustom } from './_advanced_table_column_styling_background_custom.jsx'
52
- export { default as AdvancedTableCascadeCollapse } from './_advanced_table_cascade_collapse.jsx'
52
+ export { default as AdvancedTableCascadeCollapse } from './_advanced_table_cascade_collapse.jsx'
53
+ export { default as AdvancedTableSortParentOnly } from './_advanced_table_sort_parent_only.jsx'
@@ -196,6 +196,17 @@ export default class PbAdvancedTable extends PbEnhancedElement {
196
196
  if (table.dataset.pbAdvancedTableInitialized) return;
197
197
  table.dataset.pbAdvancedTableInitialized = "true";
198
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
+
199
210
  // Delegate checkbox changes
200
211
  table.addEventListener("change", (event) => {
201
212
  const checkbox = event.target.closest('input[type="checkbox"]');
@@ -315,9 +326,64 @@ export default class PbAdvancedTable extends PbEnhancedElement {
315
326
  lastVisibleRow.classList.add("last-visible-row");
316
327
  lastVisibleRow.classList.add("last-row-cell");
317
328
  }
329
+
330
+ PbAdvancedTable.updateStickyHeaderRowHeights(parentElement);
331
+ PbAdvancedTable.updatePinnedRowsStickyTops(table);
318
332
  }
319
333
  }
320
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
+
321
387
  hideCloseIcon() {
322
388
  const closeIcon = this.element.querySelector(UP_ARROW_SELECTOR);
323
389
  closeIcon.style.display = "none";
@@ -519,3 +585,19 @@ window.expandAllRows = (element) => {
519
585
  window.expandAllSubRows = (element, rowDepth) => {
520
586
  PbAdvancedTable.handleToggleAllSubRows(element, rowDepth);
521
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
+ }