@mmlogic/components 0.5.11 → 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.
@@ -6645,6 +6645,10 @@ const MrdTable = class {
6645
6645
  this.resizeObserver = null;
6646
6646
  /** Guards against scheduling more than one column-width measurement per freeze. */
6647
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;
6648
6652
  /** Drag origin of the running column resize (null = no drag in progress). */
6649
6653
  this.resizeOrigin = null;
6650
6654
  /** True once the user has dragged (or auto-fitted) a column — see colWidthsUserSet usages. */
@@ -6739,6 +6743,7 @@ const MrdTable = class {
6739
6743
  this.colWidths = [];
6740
6744
  this.colWidthMeasureScheduled = false;
6741
6745
  this.colWidthsUserSet = false;
6746
+ this.fixedContentMeasured = false;
6742
6747
  }
6743
6748
  // ── Lifecycle ──────────────────────────────────────────────────────────────
6744
6749
  componentWillLoad() {
@@ -6782,6 +6787,7 @@ const MrdTable = class {
6782
6787
  this.colWidths = [];
6783
6788
  this.colWidthMeasureScheduled = false;
6784
6789
  this.colWidthsUserSet = false;
6790
+ this.fixedContentMeasured = false;
6785
6791
  this.scrollTop = 0;
6786
6792
  this.renderStart = 0;
6787
6793
  // Always fill the visible viewport on init — totalElements may be stale from a
@@ -6855,6 +6861,8 @@ const MrdTable = class {
6855
6861
  if (data.total != null)
6856
6862
  this.aggregationsTotal = data.total;
6857
6863
  this.aggregations = data;
6864
+ // Fresh totals can be wider than the ones the columns were checked against.
6865
+ this.fixedContentMeasured = false;
6858
6866
  }
6859
6867
  // ── Lifecycle ──────────────────────────────────────────────────────────────
6860
6868
  disconnectedCallback() {
@@ -6885,6 +6893,7 @@ const MrdTable = class {
6885
6893
  }
6886
6894
  componentDidRender() {
6887
6895
  this.freezeColWidths();
6896
+ this.growToFixedContent();
6888
6897
  }
6889
6898
  /** Freeze the column widths once the first data page has rendered.
6890
6899
  *
@@ -6938,6 +6947,38 @@ const MrdTable = class {
6938
6947
  const content = scrollWidth > clientWidth ? scrollWidth + paddingRight : clientWidth;
6939
6948
  return Math.ceil(content + borders);
6940
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
+ }
6941
6982
  /** Width each column needs for its header and its rendered page-0 cells — what the columns
6942
6983
  * are locked to, so they never end up narrower than their content.
6943
6984
  *
@@ -6951,11 +6992,11 @@ const MrdTable = class {
6951
6992
  if (ths.length === 0 || ths.length !== this.columns.length)
6952
6993
  return null;
6953
6994
  const needed = Array.from(ths).map((th, idx) => {
6954
- let widest = this.neededWidth(th);
6995
+ let widest = Math.max(this.neededWidth(th), this.neededHeaderWidth(th));
6955
6996
  // .mrd-table__cell only matches real data cells — the shimmer/retry rows render a single
6956
6997
  // colSpan cell (without that class) which would measure as the full table width.
6957
6998
  this.el
6958
- .querySelectorAll(`.mrd-table__cell:nth-child(${idx + 1})`)
6999
+ .querySelectorAll(this.columnCellSelector(idx))
6959
7000
  .forEach(cell => { widest = Math.max(widest, this.neededWidth(cell)); });
6960
7001
  return widest;
6961
7002
  });
@@ -6965,6 +7006,50 @@ const MrdTable = class {
6965
7006
  return null;
6966
7007
  return needed.map(w => Math.min(Math.max(w, MIN_COL_WIDTH), MAX_AUTOFIT_WIDTH));
6967
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;
7051
+ });
7052
+ }
6968
7053
  /** Re-reads the rendered header widths into colWidths so a drag starts from what is
6969
7054
  * actually on screen — and locks the columns if they weren't locked yet.
6970
7055
  *
@@ -7023,9 +7108,14 @@ const MrdTable = class {
7023
7108
  return;
7024
7109
  // .mrd-table__cell only matches real data cells — the shimmer/retry rows render a single
7025
7110
  // colSpan cell (without that class) which would otherwise measure as the full table width.
7026
- 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)}`);
7027
7112
  let widest = 0;
7028
- 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
+ });
7029
7119
  if (widest > 0)
7030
7120
  this.setColWidth(idx, Math.min(widest, MAX_AUTOFIT_WIDTH));
7031
7121
  }
@@ -31,6 +31,10 @@ export class MrdTable {
31
31
  this.resizeObserver = null;
32
32
  /** Guards against scheduling more than one column-width measurement per freeze. */
33
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;
34
38
  /** Drag origin of the running column resize (null = no drag in progress). */
35
39
  this.resizeOrigin = null;
36
40
  /** True once the user has dragged (or auto-fitted) a column — see colWidthsUserSet usages. */
@@ -125,6 +129,7 @@ export class MrdTable {
125
129
  this.colWidths = [];
126
130
  this.colWidthMeasureScheduled = false;
127
131
  this.colWidthsUserSet = false;
132
+ this.fixedContentMeasured = false;
128
133
  }
129
134
  // ── Lifecycle ──────────────────────────────────────────────────────────────
130
135
  componentWillLoad() {
@@ -168,6 +173,7 @@ export class MrdTable {
168
173
  this.colWidths = [];
169
174
  this.colWidthMeasureScheduled = false;
170
175
  this.colWidthsUserSet = false;
176
+ this.fixedContentMeasured = false;
171
177
  this.scrollTop = 0;
172
178
  this.renderStart = 0;
173
179
  // Always fill the visible viewport on init — totalElements may be stale from a
@@ -241,6 +247,8 @@ export class MrdTable {
241
247
  if (data.total != null)
242
248
  this.aggregationsTotal = data.total;
243
249
  this.aggregations = data;
250
+ // Fresh totals can be wider than the ones the columns were checked against.
251
+ this.fixedContentMeasured = false;
244
252
  }
245
253
  // ── Lifecycle ──────────────────────────────────────────────────────────────
246
254
  disconnectedCallback() {
@@ -271,6 +279,7 @@ export class MrdTable {
271
279
  }
272
280
  componentDidRender() {
273
281
  this.freezeColWidths();
282
+ this.growToFixedContent();
274
283
  }
275
284
  /** Freeze the column widths once the first data page has rendered.
276
285
  *
@@ -324,6 +333,38 @@ export class MrdTable {
324
333
  const content = scrollWidth > clientWidth ? scrollWidth + paddingRight : clientWidth;
325
334
  return Math.ceil(content + borders);
326
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
+ }
327
368
  /** Width each column needs for its header and its rendered page-0 cells — what the columns
328
369
  * are locked to, so they never end up narrower than their content.
329
370
  *
@@ -337,11 +378,11 @@ export class MrdTable {
337
378
  if (ths.length === 0 || ths.length !== this.columns.length)
338
379
  return null;
339
380
  const needed = Array.from(ths).map((th, idx) => {
340
- let widest = this.neededWidth(th);
381
+ let widest = Math.max(this.neededWidth(th), this.neededHeaderWidth(th));
341
382
  // .mrd-table__cell only matches real data cells — the shimmer/retry rows render a single
342
383
  // colSpan cell (without that class) which would measure as the full table width.
343
384
  this.el
344
- .querySelectorAll(`.mrd-table__cell:nth-child(${idx + 1})`)
385
+ .querySelectorAll(this.columnCellSelector(idx))
345
386
  .forEach(cell => { widest = Math.max(widest, this.neededWidth(cell)); });
346
387
  return widest;
347
388
  });
@@ -351,6 +392,50 @@ export class MrdTable {
351
392
  return null;
352
393
  return needed.map(w => Math.min(Math.max(w, MIN_COL_WIDTH), MAX_AUTOFIT_WIDTH));
353
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;
437
+ });
438
+ }
354
439
  /** Re-reads the rendered header widths into colWidths so a drag starts from what is
355
440
  * actually on screen — and locks the columns if they weren't locked yet.
356
441
  *
@@ -409,9 +494,14 @@ export class MrdTable {
409
494
  return;
410
495
  // .mrd-table__cell only matches real data cells — the shimmer/retry rows render a single
411
496
  // colSpan cell (without that class) which would otherwise measure as the full table width.
412
- 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)}`);
413
498
  let widest = 0;
414
- 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
+ });
415
505
  if (widest > 0)
416
506
  this.setColWidth(idx, Math.min(widest, MAX_AUTOFIT_WIDTH));
417
507
  }