@appius-fr/apx 2.7.0 → 2.7.1

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.
@@ -0,0 +1,37 @@
1
+ # Changelog — Scrollable Table module
2
+
3
+ All notable changes to this module are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
+
7
+ ## [2.7.1] - 2025-02-16
8
+
9
+ ### Added
10
+ - Option **`height`**: fixed body height (number or CSS length). When set, the tbody row always uses this height; `maxHeight` is ignored. Class `apx-scrollable-table--body-height` is applied.
11
+ - Option **`bodyHeightDynamic`**: dynamic body height — `{ get: (table) => number|string, useAs: 'height'|'maxHeight', updateOn?: { scroll?, resize?, scrollOn?, resizeOn? } }`. Re-resolves body size when scroll/resize sources fire (viewport, scrollbar appearance, or custom elements). **updateOn** uses booleans `scroll`/`resize` (default true = document scroll + window resize) or explicit `scrollOn`/`resizeOn` arrays with sentinels `'document'`/`'window'` or elements (ResizeObserver for resize elements). Lazy cleanup: tables removed from the DOM are dropped from updates on the next event (no MutationObserver).
12
+ - Option **`gridTemplateColumns`**: full override for column tracks (e.g. `'1fr 2fr 1fr'`). Skips width measurement.
13
+ - Option **`gridTemplateRows`**: per-section override `{ thead?, tbody?, tfoot? }` for row tracks.
14
+ - Option **`columnOverrides`**: hybrid mode — column index → CSS value (object or array). Columns not overridden keep measured width.
15
+ - Option **`rowOverrides`**: hybrid mode — per-section row index → CSS value. Rows not overridden keep measured height.
16
+ - **Column width preservation**: widths are measured from the table’s natural layout (before applying the module) and applied as `grid-template-columns`; stored in ref for refresh.
17
+ - **Row height preservation**: each `<tr>` height is measured and applied so rowspan cells keep correct proportions.
18
+ - **Scrollbar gutter**: dedicated grid column for the vertical scrollbar so it does not overlap the last data column. CSS variable `--apx-scrollable-gutter-width` (default 17px); set to `0` to allow overlap.
19
+ - CSS variables **`--apx-scrollable-thead-template-rows`**, **`--apx-scrollable-tbody-template-rows`**, **`--apx-scrollable-tfoot-template-rows`** for row track overrides.
20
+
21
+ ### Fixed
22
+ - **Horizontal scrollbar on init**: the grid class and template-columns were applied in the same layout pass, causing the tbody to overflow horizontally. The grid is now established first (with a forced reflow) before setting the measured template-columns.
23
+ - Measured column widths now use `fr` units instead of `px` so they remain flexible alongside the fixed gutter column.
24
+
25
+ ### Changed
26
+ - Removed `scrollbar-gutter: stable` from tbody; scrollbar space is now the dedicated gutter column.
27
+ - Grid template columns: value is set via `--apx-scrollable-template-columns` (with gutter suffix). Fallback in CSS includes gutter.
28
+ - Body size: when `height` is set, grid row uses fixed size; otherwise `fit-content(max-height)` as before. With `bodyHeightDynamic`, body size is resolved from `get(table)` at init and on scroll/resize (throttled).
29
+
30
+ ## [2.7.0] - 2025-02-16
31
+
32
+ ### Added
33
+ - Initial release: scrollable tbody with fixed thead/tfoot, CSS Grid + subgrid.
34
+ - `APX('table').scrollableTable(options)` and `scrollableTable('refresh')`.
35
+ - Option `maxHeight` (number or CSS length).
36
+ - Full colspan/rowspan support via explicit grid placement.
37
+ - Demo: `demo/modules/scrollableTable/index.html`.
@@ -34,9 +34,65 @@ APX('table.my-table').scrollableTable('refresh');
34
34
 
35
35
  ## Options
36
36
 
37
- | Option | Type | Default | Description |
38
- | ----------- | ---------------- | --------- | ------------------------------------ |
39
- | `maxHeight` | `number` or `string` | `'200px'` | Max height of the tbody. Number is treated as pixels; string can be any CSS length (e.g. `'50vh'`, `'20rem'`). |
37
+ | Option | Type | Default | Description |
38
+ | ---------------------- | ---------------- | --------- | --------------------------------------------------------------------------- |
39
+ | `maxHeight` | `number` or `string` | `'200px'` | Max height of the tbody (content can be shorter). Number = pixels; string = any CSS length (e.g. `'50vh'`). Ignored when `height` or `bodyHeightDynamic` is set. |
40
+ | `height` | `number` or `string` | — | Fixed height of the tbody (same syntax as `maxHeight`). When set, the tbody row always uses this height even with few rows. Ignored when `bodyHeightDynamic` is set. |
41
+ | `bodyHeightDynamic` | `object` | — | **Dynamic body height:** `{ get: (table) => number|string, useAs: 'height'\|'maxHeight', updateOn?: { scroll?, resize?, scrollOn?, resizeOn? } }`. Re-resolves height when scroll/resize sources fire. See [Dynamic body height](#dynamic-body-height) below. |
42
+ | `gridTemplateColumns` | `string` | — | CSS value for column tracks (e.g. `'1fr 2fr 1fr'`). When set, measured widths are skipped and this value is used as-is. |
43
+ | `gridTemplateRows` | `object` | — | Per-section row tracks: `{ thead?: string, tbody?: string, tfoot?: string }` (e.g. `{ tbody: 'auto 40px 40px' }`). When set for a section, measured row heights are skipped for that section. |
44
+ | `rowOverrides` | `object` | — | **Hybrid mode (rows):** per-section row index → CSS value. E.g. `{ tbody: { 0: '48px', 2: '2fr' } }` or `{ thead: ['50px', null] }`. Rows not overridden keep the measured height. Ignored when `gridTemplateRows` is set for that section. |
45
+ | `columnOverrides` | `object` or `array` | — | **Hybrid mode:** column index → CSS value. Columns not overridden keep the measured width. E.g. `{ 0: '2fr', 2: '80px' }` or `['2fr', null, '80px']`. Ignored when `gridTemplateColumns` is set. |
46
+ | `resizeObserver` | `boolean` | `false` | When `true`, observes the table size and re-applies column widths proportionally on resize. (Ignored when `gridTemplateColumns` is set.) |
47
+
48
+ ### Dynamic body height
49
+
50
+ When `bodyHeightDynamic` is set, the tbody size is computed by `get(table)` (returning a number or CSS length string) and applied as `height` or `maxHeight` according to `useAs`. The value is re-resolved when the sources in `updateOn` fire, so the table can adapt to viewport resize or scrollbar appearance.
51
+
52
+ **updateOn** (optional) controls where we listen:
53
+
54
+ - **Booleans:** `scroll` (default `false`) / `resize` (default `true` when omitted). When `scroll` is `true`, we listen to document/window scroll. When `resize` is `true`, we listen to window resize and observe `document.documentElement` (so scrollbar appearance triggers an update). Set to `false` to disable.
55
+ - **Explicit sources:** `scrollOn` / `resizeOn` — arrays of targets. Use sentinels `'document'` (scroll) and `'window'` (resize), or pass elements (we listen to `scroll` on them, or observe them with `ResizeObserver` for resize). When provided (non-empty), they override the boolean defaults.
56
+
57
+ Example: viewport-based max-height with default resize (scrollbar pop + window resize):
58
+
59
+ ```js
60
+ APX('table.my-table').scrollableTable({
61
+ bodyHeightDynamic: {
62
+ get: (table) => window.innerHeight - table.getBoundingClientRect().top - 40,
63
+ useAs: 'maxHeight',
64
+ // updateOn omitted => scroll: false, resize: true (window + ResizeObserver on document)
65
+ }
66
+ });
67
+ ```
68
+
69
+ Example: also react to a scrollable parent:
70
+
71
+ ```js
72
+ APX('table.my-table').scrollableTable({
73
+ bodyHeightDynamic: {
74
+ get: (table) => ...,
75
+ useAs: 'maxHeight',
76
+ updateOn: { scrollOn: ['document', document.querySelector('#main')], resizeOn: ['window'] }
77
+ }
78
+ });
79
+ ```
80
+
81
+ **Cleanup:** Tables removed from the DOM are removed from the update set on the next scroll/resize (lazy cleanup via `isConnected`). No MutationObserver is used.
82
+
83
+ ## CSS variables (override)
84
+
85
+ You can override row tracks in CSS via custom properties (e.g. on the table or a parent):
86
+
87
+ - `--apx-scrollable-thead-template-rows` — thead row tracks (e.g. `50px 50px`)
88
+ - `--apx-scrollable-tbody-template-rows` — tbody row tracks
89
+ - `--apx-scrollable-tfoot-template-rows` — tfoot row tracks
90
+
91
+ If set, they override the measured or default row heights for that section.
92
+
93
+ A dedicated column is reserved for the vertical scrollbar so it does not overlap the last data column. Its width is controlled by:
94
+
95
+ - `--apx-scrollable-gutter-width` — default `17px`. Set to `0` if you want the scrollbar to overlap (e.g. with thin scrollbars).
40
96
 
41
97
  ## Colspan and rowspan
42
98
 
@@ -1,60 +1,67 @@
1
- /* APX Scrollable Table: scrollable tbody with fixed thead/tfoot and aligned columns (CSS Grid + subgrid) */
1
+ /* APX Scrollable Table: scrollable tbody with fixed thead/tfoot and aligned columns (CSS Grid + subgrid)
2
+ Use !important so page styles (e.g. display: table) do not override the grid layout. */
2
3
 
4
+ /* --apx-scrollable-template-columns: set by JS (measured widths + gutter); fallback = equal columns + gutter */
5
+ /* --apx-scrollable-gutter-width: space for vertical scrollbar so it does not overlap the last column (set to 0 to allow overlap) */
6
+ /* --apx-scrollable-body-height: when class apx-scrollable-table--body-height is set, tbody row uses fixed height instead of max-height */
3
7
  .apx-scrollable-table {
4
- display: grid;
5
- grid-template-columns: repeat(var(--apx-scrollable-cols, 3), minmax(0, 1fr));
6
- grid-template-rows: auto fit-content(var(--apx-scrollable-body-max-height, 200px)) auto;
8
+ display: grid !important;
9
+ grid-template-columns: var(--apx-scrollable-template-columns, repeat(var(--apx-scrollable-cols, 3), minmax(0, 1fr)) minmax(var(--apx-scrollable-gutter-width, 17px), var(--apx-scrollable-gutter-width, 17px)));
10
+ grid-template-rows: auto var(--apx-scrollable-body-row-size, fit-content(var(--apx-scrollable-body-max-height, 200px))) auto;
7
11
  width: 100%;
12
+ table-layout: unset !important;
13
+ }
14
+
15
+ .apx-scrollable-table--body-height {
16
+ --apx-scrollable-body-row-size: var(--apx-scrollable-body-max-height, 200px);
8
17
  }
9
18
 
10
19
  /* Table with no tfoot: thead + tbody only */
11
20
  .apx-scrollable-table:not(.apx-scrollable-table--has-tfoot) {
12
- grid-template-rows: auto fit-content(var(--apx-scrollable-body-max-height, 200px));
21
+ grid-template-rows: auto var(--apx-scrollable-body-row-size, fit-content(var(--apx-scrollable-body-max-height, 200px)));
13
22
  }
14
23
 
15
24
  /* Table with no thead: tbody + tfoot only */
16
25
  .apx-scrollable-table.apx-scrollable-table--no-thead {
17
- grid-template-rows: fit-content(var(--apx-scrollable-body-max-height, 200px)) auto;
26
+ grid-template-rows: var(--apx-scrollable-body-row-size, fit-content(var(--apx-scrollable-body-max-height, 200px))) auto;
18
27
  }
19
28
 
20
29
  /* Table with neither thead nor tfoot: tbody only */
21
30
  .apx-scrollable-table.apx-scrollable-table--no-thead:not(.apx-scrollable-table--has-tfoot) {
22
- grid-template-rows: fit-content(var(--apx-scrollable-body-max-height, 200px));
31
+ grid-template-rows: var(--apx-scrollable-body-row-size, fit-content(var(--apx-scrollable-body-max-height, 200px)));
23
32
  }
24
33
 
34
+ /* --apx-scrollable-*-template-rows: set by JS (measured heights) or override in CSS / via options */
25
35
  .apx-scrollable-table > thead,
26
36
  .apx-scrollable-table > tbody,
27
37
  .apx-scrollable-table > tfoot {
28
- display: grid;
38
+ display: grid !important;
29
39
  grid-template-columns: subgrid;
30
40
  grid-column: 1 / -1;
31
- grid-template-rows: repeat(var(--apx-scrollable-thead-rows, 1), auto);
32
41
  min-height: 0;
33
42
  }
34
43
 
35
44
  .apx-scrollable-table > thead {
36
- grid-template-rows: repeat(var(--apx-scrollable-thead-rows, 1), auto);
45
+ grid-template-rows: var(--apx-scrollable-thead-template-rows, repeat(var(--apx-scrollable-thead-rows, 1), auto));
37
46
  }
38
47
 
39
48
  .apx-scrollable-table > tbody {
40
- grid-template-rows: repeat(var(--apx-scrollable-tbody-rows, 1), auto);
49
+ grid-template-rows: var(--apx-scrollable-tbody-template-rows, repeat(var(--apx-scrollable-tbody-rows, 1), auto));
41
50
  overflow: auto;
42
- /* Reserve space for the vertical scrollbar so it doesn't shrink content width and trigger a horizontal scrollbar */
43
- scrollbar-gutter: stable;
44
51
  }
45
52
 
46
53
  .apx-scrollable-table > tfoot {
47
- grid-template-rows: repeat(var(--apx-scrollable-tfoot-rows, 1), auto);
54
+ grid-template-rows: var(--apx-scrollable-tfoot-template-rows, repeat(var(--apx-scrollable-tfoot-rows, 1), auto));
48
55
  }
49
56
 
50
57
  .apx-scrollable-table > thead > tr,
51
58
  .apx-scrollable-table > tbody > tr,
52
59
  .apx-scrollable-table > tfoot > tr {
53
- display: contents;
60
+ display: contents !important;
54
61
  }
55
62
 
56
63
  .apx-scrollable-table th,
57
64
  .apx-scrollable-table td {
58
- /* Grid placement set by JS (grid-row, grid-column) for colspan/rowspan */
59
65
  min-width: 0;
66
+ box-sizing: border-box;
60
67
  }