@mmlogic/components 0.5.9 → 0.5.11

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,6 +6641,8 @@ 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;
6646
6648
  /** Drag origin of the running column resize (null = no drag in progress). */
@@ -6745,6 +6747,14 @@ const MrdTable = class {
6745
6747
  }
6746
6748
  componentDidLoad() {
6747
6749
  this.staleCheckTimer = setInterval(() => this.checkStalePages(), STALE_CHECK_INTERVAL_MS);
6750
+ // A table inside a hidden tab pane renders and loads page 0 without ever having a box, so
6751
+ // the width freeze cannot measure anything. ResizeObserver fires the moment the pane is
6752
+ // shown (the host gains a box) — that is when the freeze gets its chance. componentDidRender
6753
+ // is no help there: showing a sibling tab does not re-render this component.
6754
+ if (typeof ResizeObserver !== 'undefined') {
6755
+ this.resizeObserver = new ResizeObserver(() => this.freezeColWidths());
6756
+ this.resizeObserver.observe(this.el);
6757
+ }
6748
6758
  }
6749
6759
  // ── Helpers ────────────────────────────────────────────────────────────────
6750
6760
  applyDefaultSort(defaultSort) {
@@ -6848,6 +6858,10 @@ const MrdTable = class {
6848
6858
  }
6849
6859
  // ── Lifecycle ──────────────────────────────────────────────────────────────
6850
6860
  disconnectedCallback() {
6861
+ if (this.resizeObserver) {
6862
+ this.resizeObserver.disconnect();
6863
+ this.resizeObserver = null;
6864
+ }
6851
6865
  if (this.staleCheckTimer !== null) {
6852
6866
  clearInterval(this.staleCheckTimer);
6853
6867
  this.staleCheckTimer = null;
@@ -6870,28 +6884,35 @@ const MrdTable = class {
6870
6884
  }
6871
6885
  }
6872
6886
  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
- this.colWidthMeasureScheduled = false;
6887
- // Re-check: state may have changed (e.g. a reset) between scheduling and running.
6888
- if (this.colWidths.length !== 0)
6889
- return;
6890
- const measured = this.measureColWidths();
6891
- if (measured)
6892
- this.colWidths = measured;
6893
- });
6894
- }
6887
+ this.freezeColWidths();
6888
+ }
6889
+ /** Freeze the column widths once the first data page has rendered.
6890
+ *
6891
+ * The measurement reads layout and writes the colWidths @State, which would trigger a
6892
+ * re-render *during* the render cycle if done synchronously from componentDidRender. It is
6893
+ * deferred to a writeTask so the state mutation happens outside the render pass (avoids the
6894
+ * Stencil "state/prop changed during rendering" warning and potential loops).
6895
+ *
6896
+ * NB: the freeze must not depend on the totalElements prop — hosts may omit it and rely on
6897
+ * minKnownTotal, in which case the widths would never be locked and every incoming page
6898
+ * would re-flow all columns. */
6899
+ freezeColWidths() {
6900
+ if (this.colWidths.length !== 0 || this.colWidthMeasureScheduled || this.loadedPages.size === 0)
6901
+ return;
6902
+ this.colWidthMeasureScheduled = true;
6903
+ index.writeTask(() => {
6904
+ var _a;
6905
+ this.colWidthMeasureScheduled = false;
6906
+ // Re-check: state may have changed (e.g. a reset) between scheduling and running.
6907
+ if (this.colWidths.length !== 0)
6908
+ return;
6909
+ const measured = (_a = this.measureContentWidths()) !== null && _a !== void 0 ? _a : this.measureColWidths();
6910
+ // A table inside a hidden tab pane (a tab page that is loaded but not shown) has no
6911
+ // layout at all, so every measurement comes back 0. Locking those would pin every
6912
+ // column at MIN_COL_WIDTH for good; wait for the ResizeObserver to report a real box.
6913
+ if (measured && measured.some(w => w > 0))
6914
+ this.colWidths = measured;
6915
+ });
6895
6916
  }
6896
6917
  // ── Column resizing (drag the header border, Excel-style) ──────────────────
6897
6918
  /** Current rendered header widths, or null when they can't be trusted (header not laid out yet). */
@@ -6901,6 +6922,49 @@ const MrdTable = class {
6901
6922
  return null;
6902
6923
  return Array.from(ths).map(th => Math.round(th.getBoundingClientRect().width));
6903
6924
  }
6925
+ /** Width one cell needs to show its content in full, borders included.
6926
+ * Read from scrollWidth rather than the laid-out box: cells clip with `overflow: hidden`,
6927
+ * so a squeezed cell reports the width it *was given*, not the width it needs. */
6928
+ neededWidth(cell) {
6929
+ // The layout metrics are absent in the mock DOM the specs run on; treat them as 0
6930
+ // rather than letting a NaN poison the Math.max chain the callers build.
6931
+ const px = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
6932
+ const clientWidth = px(cell.clientWidth);
6933
+ const scrollWidth = px(cell.scrollWidth);
6934
+ const paddingRight = px(parseFloat(getComputedStyle(cell).paddingRight || '0'));
6935
+ const borders = Math.max(0, px(cell.offsetWidth) - clientWidth);
6936
+ // scrollWidth drops the right padding once the content overflows the clipped box;
6937
+ // when it fits, clientWidth is already the full padded box.
6938
+ const content = scrollWidth > clientWidth ? scrollWidth + paddingRight : clientWidth;
6939
+ return Math.ceil(content + borders);
6940
+ }
6941
+ /** Width each column needs for its header and its rendered page-0 cells — what the columns
6942
+ * are locked to, so they never end up narrower than their content.
6943
+ *
6944
+ * Deliberately not the laid-out widths: with many columns the browser squeezes them to fit
6945
+ * the container instead of overflowing (the cells clip, so they no longer resist), which
6946
+ * cost the horizontal scrollbar. Locking the needed widths makes the summed table width
6947
+ * exceed the container again, and `.mrd-table__scroll` scrolls. Capped per column at
6948
+ * MAX_AUTOFIT_WIDTH so one outlier value can't blow up the layout — same cap as auto-fit. */
6949
+ measureContentWidths() {
6950
+ const ths = this.el.querySelectorAll('.mrd-table__header');
6951
+ if (ths.length === 0 || ths.length !== this.columns.length)
6952
+ return null;
6953
+ const needed = Array.from(ths).map((th, idx) => {
6954
+ let widest = this.neededWidth(th);
6955
+ // .mrd-table__cell only matches real data cells — the shimmer/retry rows render a single
6956
+ // colSpan cell (without that class) which would measure as the full table width.
6957
+ this.el
6958
+ .querySelectorAll(`.mrd-table__cell:nth-child(${idx + 1})`)
6959
+ .forEach(cell => { widest = Math.max(widest, this.neededWidth(cell)); });
6960
+ return widest;
6961
+ });
6962
+ // Nothing has a box: the table is in a hidden tab pane. Report "cannot measure" rather
6963
+ // than clamping a row of zeroes up to MIN_COL_WIDTH, which would look like a real result.
6964
+ if (needed.every(w => w <= 0))
6965
+ return null;
6966
+ return needed.map(w => Math.min(Math.max(w, MIN_COL_WIDTH), MAX_AUTOFIT_WIDTH));
6967
+ }
6904
6968
  /** Re-reads the rendered header widths into colWidths so a drag starts from what is
6905
6969
  * actually on screen — and locks the columns if they weren't locked yet.
6906
6970
  *
@@ -6961,12 +7025,7 @@ const MrdTable = class {
6961
7025
  // colSpan cell (without that class) which would otherwise measure as the full table width.
6962
7026
  const cells = this.el.querySelectorAll(`.mrd-table__header:nth-child(${idx + 1}), .mrd-table__cell:nth-child(${idx + 1})`);
6963
7027
  let widest = 0;
6964
- cells.forEach(cell => {
6965
- // scrollWidth is the untruncated content width; cells clip with overflow: hidden.
6966
- const style = getComputedStyle(cell);
6967
- const padding = parseFloat(style.paddingLeft || '0') + parseFloat(style.paddingRight || '0');
6968
- widest = Math.max(widest, cell.scrollWidth + padding);
6969
- });
7028
+ cells.forEach(cell => { widest = Math.max(widest, this.neededWidth(cell)); });
6970
7029
  if (widest > 0)
6971
7030
  this.setColWidth(idx, Math.min(widest, MAX_AUTOFIT_WIDTH));
6972
7031
  }
@@ -27,6 +27,8 @@ 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;
32
34
  /** Drag origin of the running column resize (null = no drag in progress). */
@@ -131,6 +133,14 @@ export class MrdTable {
131
133
  }
132
134
  componentDidLoad() {
133
135
  this.staleCheckTimer = setInterval(() => this.checkStalePages(), STALE_CHECK_INTERVAL_MS);
136
+ // A table inside a hidden tab pane renders and loads page 0 without ever having a box, so
137
+ // the width freeze cannot measure anything. ResizeObserver fires the moment the pane is
138
+ // shown (the host gains a box) — that is when the freeze gets its chance. componentDidRender
139
+ // is no help there: showing a sibling tab does not re-render this component.
140
+ if (typeof ResizeObserver !== 'undefined') {
141
+ this.resizeObserver = new ResizeObserver(() => this.freezeColWidths());
142
+ this.resizeObserver.observe(this.el);
143
+ }
134
144
  }
135
145
  // ── Helpers ────────────────────────────────────────────────────────────────
136
146
  applyDefaultSort(defaultSort) {
@@ -234,6 +244,10 @@ export class MrdTable {
234
244
  }
235
245
  // ── Lifecycle ──────────────────────────────────────────────────────────────
236
246
  disconnectedCallback() {
247
+ if (this.resizeObserver) {
248
+ this.resizeObserver.disconnect();
249
+ this.resizeObserver = null;
250
+ }
237
251
  if (this.staleCheckTimer !== null) {
238
252
  clearInterval(this.staleCheckTimer);
239
253
  this.staleCheckTimer = null;
@@ -256,28 +270,35 @@ export class MrdTable {
256
270
  }
257
271
  }
258
272
  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
- this.colWidthMeasureScheduled = false;
273
- // Re-check: state may have changed (e.g. a reset) between scheduling and running.
274
- if (this.colWidths.length !== 0)
275
- return;
276
- const measured = this.measureColWidths();
277
- if (measured)
278
- this.colWidths = measured;
279
- });
280
- }
273
+ this.freezeColWidths();
274
+ }
275
+ /** Freeze the column widths once the first data page has rendered.
276
+ *
277
+ * The measurement reads layout and writes the colWidths @State, which would trigger a
278
+ * re-render *during* the render cycle if done synchronously from componentDidRender. It is
279
+ * deferred to a writeTask so the state mutation happens outside the render pass (avoids the
280
+ * Stencil "state/prop changed during rendering" warning and potential loops).
281
+ *
282
+ * NB: the freeze must not depend on the totalElements prop — hosts may omit it and rely on
283
+ * minKnownTotal, in which case the widths would never be locked and every incoming page
284
+ * would re-flow all columns. */
285
+ freezeColWidths() {
286
+ if (this.colWidths.length !== 0 || this.colWidthMeasureScheduled || this.loadedPages.size === 0)
287
+ return;
288
+ this.colWidthMeasureScheduled = true;
289
+ writeTask(() => {
290
+ var _a;
291
+ this.colWidthMeasureScheduled = false;
292
+ // Re-check: state may have changed (e.g. a reset) between scheduling and running.
293
+ if (this.colWidths.length !== 0)
294
+ return;
295
+ const measured = (_a = this.measureContentWidths()) !== null && _a !== void 0 ? _a : this.measureColWidths();
296
+ // A table inside a hidden tab pane (a tab page that is loaded but not shown) has no
297
+ // layout at all, so every measurement comes back 0. Locking those would pin every
298
+ // column at MIN_COL_WIDTH for good; wait for the ResizeObserver to report a real box.
299
+ if (measured && measured.some(w => w > 0))
300
+ this.colWidths = measured;
301
+ });
281
302
  }
282
303
  // ── Column resizing (drag the header border, Excel-style) ──────────────────
283
304
  /** Current rendered header widths, or null when they can't be trusted (header not laid out yet). */
@@ -287,6 +308,49 @@ export class MrdTable {
287
308
  return null;
288
309
  return Array.from(ths).map(th => Math.round(th.getBoundingClientRect().width));
289
310
  }
311
+ /** Width one cell needs to show its content in full, borders included.
312
+ * Read from scrollWidth rather than the laid-out box: cells clip with `overflow: hidden`,
313
+ * so a squeezed cell reports the width it *was given*, not the width it needs. */
314
+ neededWidth(cell) {
315
+ // The layout metrics are absent in the mock DOM the specs run on; treat them as 0
316
+ // rather than letting a NaN poison the Math.max chain the callers build.
317
+ const px = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
318
+ const clientWidth = px(cell.clientWidth);
319
+ const scrollWidth = px(cell.scrollWidth);
320
+ const paddingRight = px(parseFloat(getComputedStyle(cell).paddingRight || '0'));
321
+ const borders = Math.max(0, px(cell.offsetWidth) - clientWidth);
322
+ // scrollWidth drops the right padding once the content overflows the clipped box;
323
+ // when it fits, clientWidth is already the full padded box.
324
+ const content = scrollWidth > clientWidth ? scrollWidth + paddingRight : clientWidth;
325
+ return Math.ceil(content + borders);
326
+ }
327
+ /** Width each column needs for its header and its rendered page-0 cells — what the columns
328
+ * are locked to, so they never end up narrower than their content.
329
+ *
330
+ * Deliberately not the laid-out widths: with many columns the browser squeezes them to fit
331
+ * the container instead of overflowing (the cells clip, so they no longer resist), which
332
+ * cost the horizontal scrollbar. Locking the needed widths makes the summed table width
333
+ * exceed the container again, and `.mrd-table__scroll` scrolls. Capped per column at
334
+ * MAX_AUTOFIT_WIDTH so one outlier value can't blow up the layout — same cap as auto-fit. */
335
+ measureContentWidths() {
336
+ const ths = this.el.querySelectorAll('.mrd-table__header');
337
+ if (ths.length === 0 || ths.length !== this.columns.length)
338
+ return null;
339
+ const needed = Array.from(ths).map((th, idx) => {
340
+ let widest = this.neededWidth(th);
341
+ // .mrd-table__cell only matches real data cells — the shimmer/retry rows render a single
342
+ // colSpan cell (without that class) which would measure as the full table width.
343
+ this.el
344
+ .querySelectorAll(`.mrd-table__cell:nth-child(${idx + 1})`)
345
+ .forEach(cell => { widest = Math.max(widest, this.neededWidth(cell)); });
346
+ return widest;
347
+ });
348
+ // Nothing has a box: the table is in a hidden tab pane. Report "cannot measure" rather
349
+ // than clamping a row of zeroes up to MIN_COL_WIDTH, which would look like a real result.
350
+ if (needed.every(w => w <= 0))
351
+ return null;
352
+ return needed.map(w => Math.min(Math.max(w, MIN_COL_WIDTH), MAX_AUTOFIT_WIDTH));
353
+ }
290
354
  /** Re-reads the rendered header widths into colWidths so a drag starts from what is
291
355
  * actually on screen — and locks the columns if they weren't locked yet.
292
356
  *
@@ -347,12 +411,7 @@ export class MrdTable {
347
411
  // colSpan cell (without that class) which would otherwise measure as the full table width.
348
412
  const cells = this.el.querySelectorAll(`.mrd-table__header:nth-child(${idx + 1}), .mrd-table__cell:nth-child(${idx + 1})`);
349
413
  let widest = 0;
350
- cells.forEach(cell => {
351
- // scrollWidth is the untruncated content width; cells clip with overflow: hidden.
352
- const style = getComputedStyle(cell);
353
- const padding = parseFloat(style.paddingLeft || '0') + parseFloat(style.paddingRight || '0');
354
- widest = Math.max(widest, cell.scrollWidth + padding);
355
- });
414
+ cells.forEach(cell => { widest = Math.max(widest, this.neededWidth(cell)); });
356
415
  if (widest > 0)
357
416
  this.setColWidth(idx, Math.min(widest, MAX_AUTOFIT_WIDTH));
358
417
  }