@mmlogic/components 0.5.10 → 0.5.12

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.
@@ -6641,8 +6641,14 @@ const MrdTable = class {
6641
6641
  this.keydownHandler = null;
6642
6642
  this.createPickerClickHandler = null;
6643
6643
  this.viewPopoverClickHandler = null;
6644
+ /** Watches the host for gaining a box (e.g. its tab pane being shown) — see componentDidLoad. */
6645
+ this.resizeObserver = null;
6644
6646
  /** Guards against scheduling more than one column-width measurement per freeze. */
6645
6647
  this.colWidthMeasureScheduled = false;
6648
+ /** True once the header/totals widths have been checked against the current totals row. */
6649
+ this.fixedContentMeasured = false;
6650
+ /** Guards against scheduling more than one grow pass. */
6651
+ this.growScheduled = false;
6646
6652
  /** Drag origin of the running column resize (null = no drag in progress). */
6647
6653
  this.resizeOrigin = null;
6648
6654
  /** True once the user has dragged (or auto-fitted) a column — see colWidthsUserSet usages. */
@@ -6737,6 +6743,7 @@ const MrdTable = class {
6737
6743
  this.colWidths = [];
6738
6744
  this.colWidthMeasureScheduled = false;
6739
6745
  this.colWidthsUserSet = false;
6746
+ this.fixedContentMeasured = false;
6740
6747
  }
6741
6748
  // ── Lifecycle ──────────────────────────────────────────────────────────────
6742
6749
  componentWillLoad() {
@@ -6745,6 +6752,14 @@ const MrdTable = class {
6745
6752
  }
6746
6753
  componentDidLoad() {
6747
6754
  this.staleCheckTimer = setInterval(() => this.checkStalePages(), STALE_CHECK_INTERVAL_MS);
6755
+ // A table inside a hidden tab pane renders and loads page 0 without ever having a box, so
6756
+ // the width freeze cannot measure anything. ResizeObserver fires the moment the pane is
6757
+ // shown (the host gains a box) — that is when the freeze gets its chance. componentDidRender
6758
+ // is no help there: showing a sibling tab does not re-render this component.
6759
+ if (typeof ResizeObserver !== 'undefined') {
6760
+ this.resizeObserver = new ResizeObserver(() => this.freezeColWidths());
6761
+ this.resizeObserver.observe(this.el);
6762
+ }
6748
6763
  }
6749
6764
  // ── Helpers ────────────────────────────────────────────────────────────────
6750
6765
  applyDefaultSort(defaultSort) {
@@ -6772,6 +6787,7 @@ const MrdTable = class {
6772
6787
  this.colWidths = [];
6773
6788
  this.colWidthMeasureScheduled = false;
6774
6789
  this.colWidthsUserSet = false;
6790
+ this.fixedContentMeasured = false;
6775
6791
  this.scrollTop = 0;
6776
6792
  this.renderStart = 0;
6777
6793
  // Always fill the visible viewport on init — totalElements may be stale from a
@@ -6845,9 +6861,15 @@ const MrdTable = class {
6845
6861
  if (data.total != null)
6846
6862
  this.aggregationsTotal = data.total;
6847
6863
  this.aggregations = data;
6864
+ // Fresh totals can be wider than the ones the columns were checked against.
6865
+ this.fixedContentMeasured = false;
6848
6866
  }
6849
6867
  // ── Lifecycle ──────────────────────────────────────────────────────────────
6850
6868
  disconnectedCallback() {
6869
+ if (this.resizeObserver) {
6870
+ this.resizeObserver.disconnect();
6871
+ this.resizeObserver = null;
6872
+ }
6851
6873
  if (this.staleCheckTimer !== null) {
6852
6874
  clearInterval(this.staleCheckTimer);
6853
6875
  this.staleCheckTimer = null;
@@ -6870,29 +6892,36 @@ const MrdTable = class {
6870
6892
  }
6871
6893
  }
6872
6894
  componentDidRender() {
6873
- // Freeze header widths once the first data page has rendered. The measurement
6874
- // reads offsetWidth and writes the colWidths @State, which would trigger a
6875
- // re-render *during* the render cycle if done synchronously here. Defer it to a
6876
- // writeTask so the state mutation happens outside the render pass (avoids the
6877
- // Stencil "state/prop changed during rendering" warning and potential loops).
6878
- // NB: the freeze must not depend on the totalElements prop hosts may omit it
6879
- // and rely on minKnownTotal, in which case the widths would never be locked and
6880
- // every incoming page would re-flow all columns.
6881
- if (this.colWidths.length === 0 &&
6882
- !this.colWidthMeasureScheduled &&
6883
- this.loadedPages.size > 0) {
6884
- this.colWidthMeasureScheduled = true;
6885
- index.writeTask(() => {
6886
- var _a;
6887
- this.colWidthMeasureScheduled = false;
6888
- // Re-check: state may have changed (e.g. a reset) between scheduling and running.
6889
- if (this.colWidths.length !== 0)
6890
- return;
6891
- const measured = (_a = this.measureContentWidths()) !== null && _a !== void 0 ? _a : this.measureColWidths();
6892
- if (measured)
6893
- this.colWidths = measured;
6894
- });
6895
- }
6895
+ this.freezeColWidths();
6896
+ this.growToFixedContent();
6897
+ }
6898
+ /** Freeze the column widths once the first data page has rendered.
6899
+ *
6900
+ * The measurement reads layout and writes the colWidths @State, which would trigger a
6901
+ * re-render *during* the render cycle if done synchronously from componentDidRender. It is
6902
+ * deferred to a writeTask so the state mutation happens outside the render pass (avoids the
6903
+ * Stencil "state/prop changed during rendering" warning and potential loops).
6904
+ *
6905
+ * NB: the freeze must not depend on the totalElements prop — hosts may omit it and rely on
6906
+ * minKnownTotal, in which case the widths would never be locked and every incoming page
6907
+ * would re-flow all columns. */
6908
+ freezeColWidths() {
6909
+ if (this.colWidths.length !== 0 || this.colWidthMeasureScheduled || this.loadedPages.size === 0)
6910
+ return;
6911
+ this.colWidthMeasureScheduled = true;
6912
+ index.writeTask(() => {
6913
+ var _a;
6914
+ this.colWidthMeasureScheduled = false;
6915
+ // Re-check: state may have changed (e.g. a reset) between scheduling and running.
6916
+ if (this.colWidths.length !== 0)
6917
+ return;
6918
+ const measured = (_a = this.measureContentWidths()) !== null && _a !== void 0 ? _a : this.measureColWidths();
6919
+ // A table inside a hidden tab pane (a tab page that is loaded but not shown) has no
6920
+ // layout at all, so every measurement comes back 0. Locking those would pin every
6921
+ // column at MIN_COL_WIDTH for good; wait for the ResizeObserver to report a real box.
6922
+ if (measured && measured.some(w => w > 0))
6923
+ this.colWidths = measured;
6924
+ });
6896
6925
  }
6897
6926
  // ── Column resizing (drag the header border, Excel-style) ──────────────────
6898
6927
  /** Current rendered header widths, or null when they can't be trusted (header not laid out yet). */
@@ -6918,6 +6947,38 @@ const MrdTable = class {
6918
6947
  const content = scrollWidth > clientWidth ? scrollWidth + paddingRight : clientWidth;
6919
6948
  return Math.ceil(content + borders);
6920
6949
  }
6950
+ /** Width a header cell needs, measured from its children instead of the cell's own
6951
+ * scrollWidth. A header is not just text: it holds the label plus a sort icon (and a filter
6952
+ * icon once the column is filtered), with the drag handle absolutely positioned on top of
6953
+ * the right padding. Summing the in-flow children makes the icons part of the width by
6954
+ * construction — and a child's own rect is never clipped by the header's `overflow: hidden`,
6955
+ * so it stays correct even when the header is already truncated on screen.
6956
+ * Returns 0 when nothing can be measured, so callers fall back to neededWidth(). */
6957
+ neededHeaderWidth(th) {
6958
+ const px = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
6959
+ const size = (v) => px(parseFloat(v || '0'));
6960
+ const style = getComputedStyle(th);
6961
+ const paddingLeft = size(style.paddingLeft);
6962
+ const paddingRight = size(style.paddingRight);
6963
+ const borders = Math.max(0, px(th.offsetWidth) - px(th.clientWidth));
6964
+ let inflow = 0;
6965
+ let handle = 0;
6966
+ Array.from(th.children).forEach(child => {
6967
+ const el = child;
6968
+ const cs = getComputedStyle(el);
6969
+ const width = px(el.getBoundingClientRect().width) + size(cs.marginLeft) + size(cs.marginRight);
6970
+ // The drag handle is absolutely positioned: it takes no room in the flow, it overlays it.
6971
+ if (cs.position === 'absolute')
6972
+ handle = Math.max(handle, width);
6973
+ else
6974
+ inflow += width;
6975
+ });
6976
+ if (inflow <= 0)
6977
+ return 0;
6978
+ // The handle sits over the right padding; only reserve what would stick out past it,
6979
+ // so the label never ends up underneath the grab area.
6980
+ return Math.ceil(inflow + paddingLeft + paddingRight + Math.max(0, handle - paddingRight) + borders);
6981
+ }
6921
6982
  /** Width each column needs for its header and its rendered page-0 cells — what the columns
6922
6983
  * are locked to, so they never end up narrower than their content.
6923
6984
  *
@@ -6930,14 +6991,63 @@ const MrdTable = class {
6930
6991
  const ths = this.el.querySelectorAll('.mrd-table__header');
6931
6992
  if (ths.length === 0 || ths.length !== this.columns.length)
6932
6993
  return null;
6933
- return Array.from(ths).map((th, idx) => {
6934
- let widest = this.neededWidth(th);
6994
+ const needed = Array.from(ths).map((th, idx) => {
6995
+ let widest = Math.max(this.neededWidth(th), this.neededHeaderWidth(th));
6935
6996
  // .mrd-table__cell only matches real data cells — the shimmer/retry rows render a single
6936
6997
  // colSpan cell (without that class) which would measure as the full table width.
6937
6998
  this.el
6938
- .querySelectorAll(`.mrd-table__cell:nth-child(${idx + 1})`)
6999
+ .querySelectorAll(this.columnCellSelector(idx))
6939
7000
  .forEach(cell => { widest = Math.max(widest, this.neededWidth(cell)); });
6940
- return Math.min(Math.max(widest, MIN_COL_WIDTH), MAX_AUTOFIT_WIDTH);
7001
+ return widest;
7002
+ });
7003
+ // Nothing has a box: the table is in a hidden tab pane. Report "cannot measure" rather
7004
+ // than clamping a row of zeroes up to MIN_COL_WIDTH, which would look like a real result.
7005
+ if (needed.every(w => w <= 0))
7006
+ return null;
7007
+ return needed.map(w => Math.min(Math.max(w, MIN_COL_WIDTH), MAX_AUTOFIT_WIDTH));
7008
+ }
7009
+ /** Data and totals cells of one column. The totals row is included everywhere the data
7010
+ * cells are: it is fixed, always-visible content, so it must never be clipped. */
7011
+ columnCellSelector(idx) {
7012
+ return `.mrd-table__cell:nth-child(${idx + 1}), .mrd-table__totals-cell:nth-child(${idx + 1})`;
7013
+ }
7014
+ /** Width the header and totals cells of each column need. Null when nothing has a box. */
7015
+ measureFixedContentWidths() {
7016
+ const ths = this.el.querySelectorAll('.mrd-table__header');
7017
+ if (ths.length === 0 || ths.length !== this.columns.length)
7018
+ return null;
7019
+ const needed = Array.from(ths).map((th, idx) => {
7020
+ let widest = Math.max(this.neededWidth(th), this.neededHeaderWidth(th));
7021
+ this.el
7022
+ .querySelectorAll(`.mrd-table__totals-cell:nth-child(${idx + 1})`)
7023
+ .forEach(cell => { widest = Math.max(widest, this.neededWidth(cell)); });
7024
+ return widest;
7025
+ });
7026
+ return needed.every(w => w <= 0) ? null : needed;
7027
+ }
7028
+ /** Widen columns whose header or totals cell does not fit. Data cells may be ellipsised —
7029
+ * that is what the lock is for — but the header and the totals row are fixed content that
7030
+ * is always on screen, so clipping them just hides information with no way to reveal it.
7031
+ *
7032
+ * It runs as a second pass because the totals only arrive with setAggregations(), long
7033
+ * after the widths were frozen on page 0. Grow-only and once per totals change, so it can
7034
+ * never turn into the per-page re-proportioning the lock exists to prevent. Hand-set
7035
+ * widths are left alone. */
7036
+ growToFixedContent() {
7037
+ if (this.colWidths.length === 0 || this.colWidthsUserSet || this.fixedContentMeasured || this.growScheduled)
7038
+ return;
7039
+ this.growScheduled = true;
7040
+ index.writeTask(() => {
7041
+ this.growScheduled = false;
7042
+ if (this.colWidths.length === 0 || this.colWidthsUserSet)
7043
+ return;
7044
+ const needed = this.measureFixedContentWidths();
7045
+ if (!needed)
7046
+ return;
7047
+ this.fixedContentMeasured = true;
7048
+ const grown = this.colWidths.map((w, i) => Math.min(Math.max(w, needed[i]), MAX_AUTOFIT_WIDTH));
7049
+ if (grown.some((w, i) => w !== this.colWidths[i]))
7050
+ this.colWidths = grown;
6941
7051
  });
6942
7052
  }
6943
7053
  /** Re-reads the rendered header widths into colWidths so a drag starts from what is
@@ -6998,9 +7108,14 @@ const MrdTable = class {
6998
7108
  return;
6999
7109
  // .mrd-table__cell only matches real data cells — the shimmer/retry rows render a single
7000
7110
  // colSpan cell (without that class) which would otherwise measure as the full table width.
7001
- const cells = this.el.querySelectorAll(`.mrd-table__header:nth-child(${idx + 1}), .mrd-table__cell:nth-child(${idx + 1})`);
7111
+ const cells = this.el.querySelectorAll(`.mrd-table__header:nth-child(${idx + 1}), ${this.columnCellSelector(idx)}`);
7002
7112
  let widest = 0;
7003
- cells.forEach(cell => { widest = Math.max(widest, this.neededWidth(cell)); });
7113
+ cells.forEach(cell => {
7114
+ const needed = cell.classList.contains('mrd-table__header')
7115
+ ? Math.max(this.neededWidth(cell), this.neededHeaderWidth(cell))
7116
+ : this.neededWidth(cell);
7117
+ widest = Math.max(widest, needed);
7118
+ });
7004
7119
  if (widest > 0)
7005
7120
  this.setColWidth(idx, Math.min(widest, MAX_AUTOFIT_WIDTH));
7006
7121
  }
@@ -27,8 +27,14 @@ export class MrdTable {
27
27
  this.keydownHandler = null;
28
28
  this.createPickerClickHandler = null;
29
29
  this.viewPopoverClickHandler = null;
30
+ /** Watches the host for gaining a box (e.g. its tab pane being shown) — see componentDidLoad. */
31
+ this.resizeObserver = null;
30
32
  /** Guards against scheduling more than one column-width measurement per freeze. */
31
33
  this.colWidthMeasureScheduled = false;
34
+ /** True once the header/totals widths have been checked against the current totals row. */
35
+ this.fixedContentMeasured = false;
36
+ /** Guards against scheduling more than one grow pass. */
37
+ this.growScheduled = false;
32
38
  /** Drag origin of the running column resize (null = no drag in progress). */
33
39
  this.resizeOrigin = null;
34
40
  /** True once the user has dragged (or auto-fitted) a column — see colWidthsUserSet usages. */
@@ -123,6 +129,7 @@ export class MrdTable {
123
129
  this.colWidths = [];
124
130
  this.colWidthMeasureScheduled = false;
125
131
  this.colWidthsUserSet = false;
132
+ this.fixedContentMeasured = false;
126
133
  }
127
134
  // ── Lifecycle ──────────────────────────────────────────────────────────────
128
135
  componentWillLoad() {
@@ -131,6 +138,14 @@ export class MrdTable {
131
138
  }
132
139
  componentDidLoad() {
133
140
  this.staleCheckTimer = setInterval(() => this.checkStalePages(), STALE_CHECK_INTERVAL_MS);
141
+ // A table inside a hidden tab pane renders and loads page 0 without ever having a box, so
142
+ // the width freeze cannot measure anything. ResizeObserver fires the moment the pane is
143
+ // shown (the host gains a box) — that is when the freeze gets its chance. componentDidRender
144
+ // is no help there: showing a sibling tab does not re-render this component.
145
+ if (typeof ResizeObserver !== 'undefined') {
146
+ this.resizeObserver = new ResizeObserver(() => this.freezeColWidths());
147
+ this.resizeObserver.observe(this.el);
148
+ }
134
149
  }
135
150
  // ── Helpers ────────────────────────────────────────────────────────────────
136
151
  applyDefaultSort(defaultSort) {
@@ -158,6 +173,7 @@ export class MrdTable {
158
173
  this.colWidths = [];
159
174
  this.colWidthMeasureScheduled = false;
160
175
  this.colWidthsUserSet = false;
176
+ this.fixedContentMeasured = false;
161
177
  this.scrollTop = 0;
162
178
  this.renderStart = 0;
163
179
  // Always fill the visible viewport on init — totalElements may be stale from a
@@ -231,9 +247,15 @@ export class MrdTable {
231
247
  if (data.total != null)
232
248
  this.aggregationsTotal = data.total;
233
249
  this.aggregations = data;
250
+ // Fresh totals can be wider than the ones the columns were checked against.
251
+ this.fixedContentMeasured = false;
234
252
  }
235
253
  // ── Lifecycle ──────────────────────────────────────────────────────────────
236
254
  disconnectedCallback() {
255
+ if (this.resizeObserver) {
256
+ this.resizeObserver.disconnect();
257
+ this.resizeObserver = null;
258
+ }
237
259
  if (this.staleCheckTimer !== null) {
238
260
  clearInterval(this.staleCheckTimer);
239
261
  this.staleCheckTimer = null;
@@ -256,29 +278,36 @@ export class MrdTable {
256
278
  }
257
279
  }
258
280
  componentDidRender() {
259
- // Freeze header widths once the first data page has rendered. The measurement
260
- // reads offsetWidth and writes the colWidths @State, which would trigger a
261
- // re-render *during* the render cycle if done synchronously here. Defer it to a
262
- // writeTask so the state mutation happens outside the render pass (avoids the
263
- // Stencil "state/prop changed during rendering" warning and potential loops).
264
- // NB: the freeze must not depend on the totalElements prop hosts may omit it
265
- // and rely on minKnownTotal, in which case the widths would never be locked and
266
- // every incoming page would re-flow all columns.
267
- if (this.colWidths.length === 0 &&
268
- !this.colWidthMeasureScheduled &&
269
- this.loadedPages.size > 0) {
270
- this.colWidthMeasureScheduled = true;
271
- writeTask(() => {
272
- var _a;
273
- this.colWidthMeasureScheduled = false;
274
- // Re-check: state may have changed (e.g. a reset) between scheduling and running.
275
- if (this.colWidths.length !== 0)
276
- return;
277
- const measured = (_a = this.measureContentWidths()) !== null && _a !== void 0 ? _a : this.measureColWidths();
278
- if (measured)
279
- this.colWidths = measured;
280
- });
281
- }
281
+ this.freezeColWidths();
282
+ this.growToFixedContent();
283
+ }
284
+ /** Freeze the column widths once the first data page has rendered.
285
+ *
286
+ * The measurement reads layout and writes the colWidths @State, which would trigger a
287
+ * re-render *during* the render cycle if done synchronously from componentDidRender. It is
288
+ * deferred to a writeTask so the state mutation happens outside the render pass (avoids the
289
+ * Stencil "state/prop changed during rendering" warning and potential loops).
290
+ *
291
+ * NB: the freeze must not depend on the totalElements prop — hosts may omit it and rely on
292
+ * minKnownTotal, in which case the widths would never be locked and every incoming page
293
+ * would re-flow all columns. */
294
+ freezeColWidths() {
295
+ if (this.colWidths.length !== 0 || this.colWidthMeasureScheduled || this.loadedPages.size === 0)
296
+ return;
297
+ this.colWidthMeasureScheduled = true;
298
+ writeTask(() => {
299
+ var _a;
300
+ this.colWidthMeasureScheduled = false;
301
+ // Re-check: state may have changed (e.g. a reset) between scheduling and running.
302
+ if (this.colWidths.length !== 0)
303
+ return;
304
+ const measured = (_a = this.measureContentWidths()) !== null && _a !== void 0 ? _a : this.measureColWidths();
305
+ // A table inside a hidden tab pane (a tab page that is loaded but not shown) has no
306
+ // layout at all, so every measurement comes back 0. Locking those would pin every
307
+ // column at MIN_COL_WIDTH for good; wait for the ResizeObserver to report a real box.
308
+ if (measured && measured.some(w => w > 0))
309
+ this.colWidths = measured;
310
+ });
282
311
  }
283
312
  // ── Column resizing (drag the header border, Excel-style) ──────────────────
284
313
  /** Current rendered header widths, or null when they can't be trusted (header not laid out yet). */
@@ -304,6 +333,38 @@ export class MrdTable {
304
333
  const content = scrollWidth > clientWidth ? scrollWidth + paddingRight : clientWidth;
305
334
  return Math.ceil(content + borders);
306
335
  }
336
+ /** Width a header cell needs, measured from its children instead of the cell's own
337
+ * scrollWidth. A header is not just text: it holds the label plus a sort icon (and a filter
338
+ * icon once the column is filtered), with the drag handle absolutely positioned on top of
339
+ * the right padding. Summing the in-flow children makes the icons part of the width by
340
+ * construction — and a child's own rect is never clipped by the header's `overflow: hidden`,
341
+ * so it stays correct even when the header is already truncated on screen.
342
+ * Returns 0 when nothing can be measured, so callers fall back to neededWidth(). */
343
+ neededHeaderWidth(th) {
344
+ const px = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
345
+ const size = (v) => px(parseFloat(v || '0'));
346
+ const style = getComputedStyle(th);
347
+ const paddingLeft = size(style.paddingLeft);
348
+ const paddingRight = size(style.paddingRight);
349
+ const borders = Math.max(0, px(th.offsetWidth) - px(th.clientWidth));
350
+ let inflow = 0;
351
+ let handle = 0;
352
+ Array.from(th.children).forEach(child => {
353
+ const el = child;
354
+ const cs = getComputedStyle(el);
355
+ const width = px(el.getBoundingClientRect().width) + size(cs.marginLeft) + size(cs.marginRight);
356
+ // The drag handle is absolutely positioned: it takes no room in the flow, it overlays it.
357
+ if (cs.position === 'absolute')
358
+ handle = Math.max(handle, width);
359
+ else
360
+ inflow += width;
361
+ });
362
+ if (inflow <= 0)
363
+ return 0;
364
+ // The handle sits over the right padding; only reserve what would stick out past it,
365
+ // so the label never ends up underneath the grab area.
366
+ return Math.ceil(inflow + paddingLeft + paddingRight + Math.max(0, handle - paddingRight) + borders);
367
+ }
307
368
  /** Width each column needs for its header and its rendered page-0 cells — what the columns
308
369
  * are locked to, so they never end up narrower than their content.
309
370
  *
@@ -316,14 +377,63 @@ export class MrdTable {
316
377
  const ths = this.el.querySelectorAll('.mrd-table__header');
317
378
  if (ths.length === 0 || ths.length !== this.columns.length)
318
379
  return null;
319
- return Array.from(ths).map((th, idx) => {
320
- let widest = this.neededWidth(th);
380
+ const needed = Array.from(ths).map((th, idx) => {
381
+ let widest = Math.max(this.neededWidth(th), this.neededHeaderWidth(th));
321
382
  // .mrd-table__cell only matches real data cells — the shimmer/retry rows render a single
322
383
  // colSpan cell (without that class) which would measure as the full table width.
323
384
  this.el
324
- .querySelectorAll(`.mrd-table__cell:nth-child(${idx + 1})`)
385
+ .querySelectorAll(this.columnCellSelector(idx))
325
386
  .forEach(cell => { widest = Math.max(widest, this.neededWidth(cell)); });
326
- return Math.min(Math.max(widest, MIN_COL_WIDTH), MAX_AUTOFIT_WIDTH);
387
+ return widest;
388
+ });
389
+ // Nothing has a box: the table is in a hidden tab pane. Report "cannot measure" rather
390
+ // than clamping a row of zeroes up to MIN_COL_WIDTH, which would look like a real result.
391
+ if (needed.every(w => w <= 0))
392
+ return null;
393
+ return needed.map(w => Math.min(Math.max(w, MIN_COL_WIDTH), MAX_AUTOFIT_WIDTH));
394
+ }
395
+ /** Data and totals cells of one column. The totals row is included everywhere the data
396
+ * cells are: it is fixed, always-visible content, so it must never be clipped. */
397
+ columnCellSelector(idx) {
398
+ return `.mrd-table__cell:nth-child(${idx + 1}), .mrd-table__totals-cell:nth-child(${idx + 1})`;
399
+ }
400
+ /** Width the header and totals cells of each column need. Null when nothing has a box. */
401
+ measureFixedContentWidths() {
402
+ const ths = this.el.querySelectorAll('.mrd-table__header');
403
+ if (ths.length === 0 || ths.length !== this.columns.length)
404
+ return null;
405
+ const needed = Array.from(ths).map((th, idx) => {
406
+ let widest = Math.max(this.neededWidth(th), this.neededHeaderWidth(th));
407
+ this.el
408
+ .querySelectorAll(`.mrd-table__totals-cell:nth-child(${idx + 1})`)
409
+ .forEach(cell => { widest = Math.max(widest, this.neededWidth(cell)); });
410
+ return widest;
411
+ });
412
+ return needed.every(w => w <= 0) ? null : needed;
413
+ }
414
+ /** Widen columns whose header or totals cell does not fit. Data cells may be ellipsised —
415
+ * that is what the lock is for — but the header and the totals row are fixed content that
416
+ * is always on screen, so clipping them just hides information with no way to reveal it.
417
+ *
418
+ * It runs as a second pass because the totals only arrive with setAggregations(), long
419
+ * after the widths were frozen on page 0. Grow-only and once per totals change, so it can
420
+ * never turn into the per-page re-proportioning the lock exists to prevent. Hand-set
421
+ * widths are left alone. */
422
+ growToFixedContent() {
423
+ if (this.colWidths.length === 0 || this.colWidthsUserSet || this.fixedContentMeasured || this.growScheduled)
424
+ return;
425
+ this.growScheduled = true;
426
+ writeTask(() => {
427
+ this.growScheduled = false;
428
+ if (this.colWidths.length === 0 || this.colWidthsUserSet)
429
+ return;
430
+ const needed = this.measureFixedContentWidths();
431
+ if (!needed)
432
+ return;
433
+ this.fixedContentMeasured = true;
434
+ const grown = this.colWidths.map((w, i) => Math.min(Math.max(w, needed[i]), MAX_AUTOFIT_WIDTH));
435
+ if (grown.some((w, i) => w !== this.colWidths[i]))
436
+ this.colWidths = grown;
327
437
  });
328
438
  }
329
439
  /** Re-reads the rendered header widths into colWidths so a drag starts from what is
@@ -384,9 +494,14 @@ export class MrdTable {
384
494
  return;
385
495
  // .mrd-table__cell only matches real data cells — the shimmer/retry rows render a single
386
496
  // colSpan cell (without that class) which would otherwise measure as the full table width.
387
- const cells = this.el.querySelectorAll(`.mrd-table__header:nth-child(${idx + 1}), .mrd-table__cell:nth-child(${idx + 1})`);
497
+ const cells = this.el.querySelectorAll(`.mrd-table__header:nth-child(${idx + 1}), ${this.columnCellSelector(idx)}`);
388
498
  let widest = 0;
389
- cells.forEach(cell => { widest = Math.max(widest, this.neededWidth(cell)); });
499
+ cells.forEach(cell => {
500
+ const needed = cell.classList.contains('mrd-table__header')
501
+ ? Math.max(this.neededWidth(cell), this.neededHeaderWidth(cell))
502
+ : this.neededWidth(cell);
503
+ widest = Math.max(widest, needed);
504
+ });
390
505
  if (widest > 0)
391
506
  this.setColWidth(idx, Math.min(widest, MAX_AUTOFIT_WIDTH));
392
507
  }