bitboss-ui 3.0.0-beta.4 → 3.0.0-beta.8
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.
- package/README.md +36 -29
- package/dist/ai/BbBadge.md +10 -5
- package/dist/ai/BbSelect.md +8 -0
- package/dist/ai/BbTable.md +257 -51
- package/dist/ai/changelog.json +86 -2
- package/dist/ai/components.json +25 -15
- package/dist/ai/guides/design-language.md +14 -14
- package/dist/ai/guides/design-tokens.md +25 -18
- package/dist/ai/guides/installation-and-plugin-setup.md +1 -1
- package/dist/ai/guides/migration/components/bb-dialog.md +35 -29
- package/dist/ai/guides/migration/components/bb-table.md +406 -28
- package/dist/ai/guides/migration/v2-to-v3.md +175 -75
- package/dist/ai/index.md +1 -1
- package/dist/ai/recipes/inertia/layout-scaffold.md +5 -1
- package/dist/ai/recipes/inertia/ownership-atlas.md +41 -20
- package/dist/ai/recipes/inertia/records-workspace.md +26 -7
- package/dist/ai/recipes/nuxt/layout-scaffold.md +5 -1
- package/dist/ai/recipes/nuxt/records-workspace.md +26 -7
- package/dist/ai/recipes/vue/layout-scaffold.md +5 -1
- package/dist/ai/recipes/vue/records-workspace.md +26 -7
- package/dist/ai/source/BbBaseInputContainer.md +26 -0
- package/dist/ai/source/BbBreadcrumbs.md +31 -0
- package/dist/ai/source/BbDialog.md +15 -9
- package/dist/ai/source/BbOffCanvas.md +17 -11
- package/dist/ai/source/BbProgress.md +2 -2
- package/dist/ai/source/BbTable.md +837 -787
- package/dist/ai/source/CommonInputWrapper.md +4 -0
- package/dist/components/BbPagination/BbPagination.vue.d.ts +1 -1
- package/dist/components/BbTable/BbTable.vue.d.ts +5 -2
- package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +439 -484
- package/dist/components/BbTable/types.d.ts +83 -21
- package/dist/components/BbTable/utils.d.ts +18 -3
- package/dist/components/BbTable/utils.js +36 -28
- package/dist/index.d.ts +5 -1
- package/dist/index.js +68 -67
- package/dist/llms-full.txt +1065 -265
- package/dist/llms-medium.txt +15 -15
- package/dist/llms.txt +1 -1
- package/dist/styles.css +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/dist/assets/svgs/sort.svg_raw.js +0 -4
|
@@ -17,45 +17,51 @@
|
|
|
17
17
|
:style="containerStyle"
|
|
18
18
|
>
|
|
19
19
|
<legend v-if="selectable" class="sr-only">{{ legendText }}</legend>
|
|
20
|
-
|
|
20
|
+
<!-- The caption lives OUTSIDE the table role (a `caption` role is invalid
|
|
21
|
+
there for a div table) and names the table through `aria-labelledby`. -->
|
|
22
|
+
<div
|
|
23
|
+
v-if="caption"
|
|
24
|
+
:id="captionId"
|
|
25
|
+
class="bb-table-caption"
|
|
26
|
+
:class="{
|
|
27
|
+
'sr-only': !displayCaption,
|
|
28
|
+
}"
|
|
29
|
+
>
|
|
30
|
+
{{ caption }}
|
|
31
|
+
</div>
|
|
32
|
+
<!-- The grid root. It owns `grid-template-columns` (the tracks ARE the
|
|
33
|
+
column API); rowgroups and rows are subgrids that inherit them. The role
|
|
34
|
+
chain table → rowgroup → row → columnheader/cell must stay unbroken: a
|
|
35
|
+
single unroled wrapper between two levels collapses the whole tree to
|
|
36
|
+
generic for assistive tech. -->
|
|
37
|
+
<div
|
|
21
38
|
ref="tableRef"
|
|
39
|
+
:aria-labelledby="caption ? captionId : undefined"
|
|
22
40
|
:aria-rowcount="
|
|
23
|
-
hasProvidedAccessibilityData
|
|
41
|
+
hasProvidedAccessibilityData && contextState.totalItems != null
|
|
42
|
+
? contextState.totalItems + 1
|
|
43
|
+
: undefined
|
|
24
44
|
"
|
|
45
|
+
class="bb-table__table"
|
|
46
|
+
role="table"
|
|
47
|
+
:style="{ gridTemplateColumns: gridTemplate }"
|
|
25
48
|
>
|
|
26
|
-
<caption
|
|
27
|
-
v-if="caption"
|
|
28
|
-
class="bb-table-caption"
|
|
29
|
-
:class="{
|
|
30
|
-
'sr-only': !displayCaption,
|
|
31
|
-
}"
|
|
32
|
-
>
|
|
33
|
-
{{
|
|
34
|
-
caption
|
|
35
|
-
}}
|
|
36
|
-
</caption>
|
|
37
|
-
<colgroup>
|
|
38
|
-
<col
|
|
39
|
-
v-for="track in columnTracks"
|
|
40
|
-
:key="track.key"
|
|
41
|
-
:style="{ width: track.width }"
|
|
42
|
-
/>
|
|
43
|
-
</colgroup>
|
|
44
49
|
<!-- Inert while loading too: sorting or select-all would fire against
|
|
45
50
|
rows that are about to be replaced. -->
|
|
46
|
-
<
|
|
51
|
+
<div class="bb-table__head" :inert="contentInert" role="rowgroup">
|
|
47
52
|
<!-- @vue-ignore -->
|
|
48
53
|
<slot name="thead">
|
|
49
|
-
<
|
|
54
|
+
<div
|
|
50
55
|
:aria-rowindex="hasProvidedAccessibilityData ? 1 : undefined"
|
|
51
56
|
class="bb-table-header-row"
|
|
52
57
|
:class="headerRowClass"
|
|
58
|
+
role="row"
|
|
53
59
|
>
|
|
54
|
-
<
|
|
60
|
+
<div
|
|
55
61
|
v-if="selectable"
|
|
56
62
|
class="bb-table-header bb-table-header--select"
|
|
57
63
|
:class="thClass"
|
|
58
|
-
|
|
64
|
+
role="columnheader"
|
|
59
65
|
>
|
|
60
66
|
<!-- @vue-ignore -->
|
|
61
67
|
<slot
|
|
@@ -93,8 +99,8 @@
|
|
|
93
99
|
>
|
|
94
100
|
</span>
|
|
95
101
|
</slot>
|
|
96
|
-
</
|
|
97
|
-
<
|
|
102
|
+
</div>
|
|
103
|
+
<div
|
|
98
104
|
v-for="(header, index) in mappedHeaders"
|
|
99
105
|
:key="header.key"
|
|
100
106
|
:aria-sort="
|
|
@@ -109,8 +115,13 @@
|
|
|
109
115
|
index === mappedHeaders.length - 1,
|
|
110
116
|
},
|
|
111
117
|
]"
|
|
112
|
-
|
|
118
|
+
role="columnheader"
|
|
113
119
|
>
|
|
120
|
+
<component
|
|
121
|
+
:is="headerAffixFor(header.key, 'prepend')"
|
|
122
|
+
v-if="headerAffixFor(header.key, 'prepend')"
|
|
123
|
+
v-bind="headerAffixScope(header)"
|
|
124
|
+
/>
|
|
114
125
|
<!-- @vue-ignore -->
|
|
115
126
|
<slot
|
|
116
127
|
:classes="header.classes"
|
|
@@ -134,22 +145,39 @@
|
|
|
134
145
|
<span class="bb-table-header__content">{{
|
|
135
146
|
header.label
|
|
136
147
|
}}</span>
|
|
137
|
-
|
|
148
|
+
<!-- One up arrow (ascending, AG Grid convention); the desc state
|
|
149
|
+
rotates it 180° (the glyph is its own mirror), so the flip
|
|
150
|
+
between directions animates. -->
|
|
151
|
+
<svg
|
|
152
|
+
aria-hidden="true"
|
|
138
153
|
class="bb-table-sort__icon"
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
fill="none"
|
|
155
|
+
height="1em"
|
|
156
|
+
stroke="currentColor"
|
|
157
|
+
stroke-linecap="round"
|
|
158
|
+
stroke-linejoin="round"
|
|
159
|
+
stroke-width="2"
|
|
160
|
+
viewBox="0 0 24 24"
|
|
161
|
+
width="1em"
|
|
162
|
+
>
|
|
163
|
+
<path d="M12 20V4m-7 7l7-7l7 7" />
|
|
164
|
+
</svg>
|
|
142
165
|
</BbBaseButton>
|
|
143
166
|
<span v-else class="bb-table-header__content">{{
|
|
144
167
|
header.label
|
|
145
168
|
}}</span>
|
|
146
169
|
</slot>
|
|
147
|
-
|
|
148
|
-
|
|
170
|
+
<component
|
|
171
|
+
:is="headerAffixFor(header.key, 'append')"
|
|
172
|
+
v-if="headerAffixFor(header.key, 'append')"
|
|
173
|
+
v-bind="headerAffixScope(header)"
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
<div
|
|
149
177
|
v-if="actions"
|
|
150
178
|
class="bb-table-header bb-table-header--actions"
|
|
151
179
|
:class="thClass"
|
|
152
|
-
|
|
180
|
+
role="columnheader"
|
|
153
181
|
>
|
|
154
182
|
<!-- @vue-ignore -->
|
|
155
183
|
<slot
|
|
@@ -160,11 +188,11 @@
|
|
|
160
188
|
{{ actionsText || t('table.actionsText').value }}
|
|
161
189
|
</span>
|
|
162
190
|
</slot>
|
|
163
|
-
</
|
|
164
|
-
</
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
165
193
|
</slot>
|
|
166
|
-
</
|
|
167
|
-
<
|
|
194
|
+
</div>
|
|
195
|
+
<div class="bb-table__body" role="rowgroup">
|
|
168
196
|
<!-- Skeletons only when there is nothing to show. A refetch over
|
|
169
197
|
existing rows keeps them on screen (dimmed, with the busy bar above)
|
|
170
198
|
instead of blanking the table — see `.bb-table--loading` in the CSS. -->
|
|
@@ -174,23 +202,26 @@
|
|
|
174
202
|
name="loading"
|
|
175
203
|
>
|
|
176
204
|
<template v-for="(_, rowIndex) in skeletonLength" :key="rowIndex">
|
|
177
|
-
<
|
|
205
|
+
<div
|
|
178
206
|
:aria-rowindex="
|
|
179
207
|
hasProvidedAccessibilityData ? rowIndex + 2 : undefined
|
|
180
208
|
"
|
|
181
209
|
class="bb-table-skeleton__row"
|
|
210
|
+
role="row"
|
|
182
211
|
>
|
|
183
|
-
<
|
|
212
|
+
<div
|
|
184
213
|
v-if="selectable"
|
|
185
214
|
class="bb-table-skeleton__cell bb-table-skeleton__cell--select"
|
|
215
|
+
role="cell"
|
|
186
216
|
>
|
|
187
217
|
<div class="bb-table-skeleton__placeholder" />
|
|
188
|
-
</
|
|
189
|
-
<
|
|
218
|
+
</div>
|
|
219
|
+
<div
|
|
190
220
|
v-for="(col, colIndex) in props.columns"
|
|
191
221
|
:key="col.key"
|
|
192
222
|
class="bb-table-skeleton__cell"
|
|
193
223
|
:class="[`bb-table-skeleton__cell--${col.skeleton ?? 'text'}`]"
|
|
224
|
+
role="cell"
|
|
194
225
|
>
|
|
195
226
|
<!-- text: 2 lines with dynamic widths -->
|
|
196
227
|
<template v-if="!col.skeleton || col.skeleton === 'text'">
|
|
@@ -245,37 +276,42 @@
|
|
|
245
276
|
v-else-if="col.skeleton === 'badge'"
|
|
246
277
|
class="bb-table-skeleton__badge"
|
|
247
278
|
/>
|
|
248
|
-
</
|
|
249
|
-
<
|
|
279
|
+
</div>
|
|
280
|
+
<div
|
|
250
281
|
v-if="actions"
|
|
251
282
|
class="bb-table-skeleton__cell bb-table-skeleton__cell--actions"
|
|
283
|
+
role="cell"
|
|
252
284
|
>
|
|
253
285
|
<div class="bb-table-skeleton__placeholder" />
|
|
254
|
-
</
|
|
255
|
-
</
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
256
288
|
</template>
|
|
257
289
|
</slot>
|
|
258
290
|
<!-- The empty state's row/cell shell is the table's, not the
|
|
259
|
-
consumer's: the slot fills a full-width cell (the
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
<tr
|
|
291
|
+
consumer's: the slot fills a full-width cell (spanning the selection/
|
|
292
|
+
actions columns too) so custom content is centred across the table
|
|
293
|
+
instead of landing as stray markup inside the body. -->
|
|
294
|
+
<div
|
|
264
295
|
v-else-if="!options.length"
|
|
265
296
|
:aria-rowindex="hasProvidedAccessibilityData ? 2 : undefined"
|
|
266
297
|
class="bb-table-no-data__row"
|
|
298
|
+
role="row"
|
|
267
299
|
>
|
|
268
|
-
<
|
|
300
|
+
<div
|
|
301
|
+
:aria-colspan="replacementContentSpan"
|
|
302
|
+
class="bb-table-no-data__cell"
|
|
303
|
+
role="cell"
|
|
304
|
+
>
|
|
269
305
|
<slot name="no-data">
|
|
270
306
|
<span class="bb-table-no-data__text">{{
|
|
271
307
|
noDataText || t('common.noDataText').value
|
|
272
308
|
}}</span>
|
|
273
309
|
</slot>
|
|
274
|
-
</
|
|
275
|
-
</
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
276
312
|
<slot v-else name="tbody">
|
|
277
313
|
<template v-for="(item, index) in mappedItems" :key="item.valueHash">
|
|
278
|
-
<
|
|
314
|
+
<div
|
|
279
315
|
:aria-current="item.highlighted ? 'true' : undefined"
|
|
280
316
|
:aria-rowindex="
|
|
281
317
|
hasProvidedAccessibilityData && item.rowIndex
|
|
@@ -288,6 +324,7 @@
|
|
|
288
324
|
{ 'bb-table-data__row--highlighted': item.highlighted },
|
|
289
325
|
]"
|
|
290
326
|
:inert="contentInert"
|
|
327
|
+
role="row"
|
|
291
328
|
:tabindex="
|
|
292
329
|
keyboardNavigation
|
|
293
330
|
? index === activeRowIndex
|
|
@@ -301,10 +338,11 @@
|
|
|
301
338
|
@focus="() => onRowFocus(index)"
|
|
302
339
|
@keydown="(event) => onRowKeydown(event, index)"
|
|
303
340
|
>
|
|
304
|
-
<
|
|
341
|
+
<div
|
|
305
342
|
v-if="selectable"
|
|
306
343
|
class="bb-table-data__cell bb-table-data__cell--select"
|
|
307
344
|
:class="tdClass"
|
|
345
|
+
role="cell"
|
|
308
346
|
>
|
|
309
347
|
<!-- @vue-ignore -->
|
|
310
348
|
<slot
|
|
@@ -358,8 +396,8 @@
|
|
|
358
396
|
}}</span>
|
|
359
397
|
</label>
|
|
360
398
|
</slot>
|
|
361
|
-
</
|
|
362
|
-
<
|
|
399
|
+
</div>
|
|
400
|
+
<div
|
|
363
401
|
v-for="(col, colIndex) in item.cols"
|
|
364
402
|
:key="col.key"
|
|
365
403
|
class="bb-table-data__cell"
|
|
@@ -371,6 +409,7 @@
|
|
|
371
409
|
colIndex === item.cols.length - 1,
|
|
372
410
|
},
|
|
373
411
|
]"
|
|
412
|
+
role="cell"
|
|
374
413
|
>
|
|
375
414
|
<!-- @vue-ignore -->
|
|
376
415
|
<slot
|
|
@@ -389,11 +428,12 @@
|
|
|
389
428
|
:value="item.value"
|
|
390
429
|
>{{ col.content }}</slot
|
|
391
430
|
>
|
|
392
|
-
</
|
|
393
|
-
<
|
|
431
|
+
</div>
|
|
432
|
+
<div
|
|
394
433
|
v-if="actions"
|
|
395
434
|
class="bb-table__cell bb-table__cell--actions"
|
|
396
435
|
:class="tdClass"
|
|
436
|
+
role="cell"
|
|
397
437
|
>
|
|
398
438
|
<!-- @vue-ignore -->
|
|
399
439
|
<slot
|
|
@@ -408,9 +448,9 @@
|
|
|
408
448
|
:toggle-selected="() => toggleSelectedFor(item)"
|
|
409
449
|
:value="item.value"
|
|
410
450
|
></slot>
|
|
411
|
-
</
|
|
412
|
-
</
|
|
413
|
-
<
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
<div
|
|
414
454
|
v-if="item.expanded"
|
|
415
455
|
:id="`expanded_${item.valueHash}`"
|
|
416
456
|
:aria-rowindex="
|
|
@@ -420,10 +460,12 @@
|
|
|
420
460
|
"
|
|
421
461
|
class="bb-table-expand__row"
|
|
422
462
|
:class="{ 'bb-table-expand__row--highlighted': item.highlighted }"
|
|
463
|
+
role="row"
|
|
423
464
|
>
|
|
424
|
-
<
|
|
465
|
+
<div
|
|
466
|
+
:aria-colspan="replacementContentSpan"
|
|
425
467
|
class="bb-table-expand__cell"
|
|
426
|
-
|
|
468
|
+
role="cell"
|
|
427
469
|
>
|
|
428
470
|
<!-- @vue-ignore -->
|
|
429
471
|
<slot
|
|
@@ -438,8 +480,8 @@
|
|
|
438
480
|
:toggle-selected="() => toggleSelectedFor(item)"
|
|
439
481
|
:value="item.value"
|
|
440
482
|
></slot>
|
|
441
|
-
</
|
|
442
|
-
</
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
443
485
|
</template>
|
|
444
486
|
</slot>
|
|
445
487
|
|
|
@@ -448,8 +490,11 @@
|
|
|
448
490
|
rows alike — and now also when a custom `#loading` slot replaces the
|
|
449
491
|
skeleton, which used to swallow it. Deliberately NOT inert (unlike
|
|
450
492
|
the data rows above): `inert` would remove it from the a11y tree and
|
|
451
|
-
the table would go busy in silence.
|
|
452
|
-
|
|
493
|
+
the table would go busy in silence. The live region sits on the
|
|
494
|
+
inner text, not on the cell: a `status` role ON the cell leaves the
|
|
495
|
+
row with zero cells in the accessibility tree (a native table
|
|
496
|
+
repaired that; a div table does not). -->
|
|
497
|
+
<div
|
|
453
498
|
v-if="loading"
|
|
454
499
|
:aria-rowindex="
|
|
455
500
|
hasProvidedAccessibilityData && !options.length
|
|
@@ -457,24 +502,30 @@
|
|
|
457
502
|
: undefined
|
|
458
503
|
"
|
|
459
504
|
class="bb-table-loading__row bb-table-loading__row--sr-only"
|
|
505
|
+
role="row"
|
|
460
506
|
>
|
|
461
|
-
<
|
|
462
|
-
aria-
|
|
507
|
+
<div
|
|
508
|
+
:aria-colspan="replacementContentSpan"
|
|
463
509
|
class="bb-table-loading__cell"
|
|
464
|
-
|
|
465
|
-
role="status"
|
|
510
|
+
role="cell"
|
|
466
511
|
>
|
|
467
|
-
<span
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
512
|
+
<span
|
|
513
|
+
aria-live="polite"
|
|
514
|
+
class="bb-table-loading__text sr-only"
|
|
515
|
+
role="status"
|
|
516
|
+
>{{ loadingText || t('common.loadingText').value }}</span
|
|
517
|
+
>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
<!-- Rendered unconditionally (class parity); the rowgroup role only
|
|
522
|
+
when there is content, since an empty `rowgroup` fails the required
|
|
523
|
+
owned-elements check. -->
|
|
524
|
+
<div class="bb-table__foot" :role="slots.tfoot ? 'rowgroup' : undefined">
|
|
474
525
|
<!-- @vue-ignore -->
|
|
475
526
|
<slot :column-count="replacementContentSpan" name="tfoot"></slot>
|
|
476
|
-
</
|
|
477
|
-
</
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
478
529
|
<input v-for="input in hiddenInputs" :key="input.value" v-bind="input" />
|
|
479
530
|
</component>
|
|
480
531
|
</template>
|
|
@@ -515,8 +566,6 @@ import {
|
|
|
515
566
|
} from '@/utilities/functions/parsePaginationNumber';
|
|
516
567
|
import BaseCheckbox from '../BbBaseCheckbox/BbBaseCheckbox.vue';
|
|
517
568
|
import BaseRadio from '../BbBaseRadio/BbBaseRadio.vue';
|
|
518
|
-
import BbIcon from '../BbIcon/BbIcon.vue';
|
|
519
|
-
import sortIcon from '@/assets/svgs/sort.svg?raw';
|
|
520
569
|
import type { Option as BaseOption } from '@/types/Option';
|
|
521
570
|
import type { Classes } from '@/types/Classes';
|
|
522
571
|
import type { ColumnName } from '@/types/ColumnName';
|
|
@@ -524,10 +573,12 @@ import type { PrefixWith } from '@/types/PrefixWith';
|
|
|
524
573
|
import type {
|
|
525
574
|
BbTableColumn,
|
|
526
575
|
BbTableEvents,
|
|
576
|
+
BbTableHeaderAffixSlotProps,
|
|
527
577
|
BbTableProps,
|
|
528
578
|
BbTableSlots,
|
|
529
579
|
BbTableSortEntry,
|
|
530
580
|
ExpandControllerProps,
|
|
581
|
+
HeaderAffixSuffix,
|
|
531
582
|
ItemKey,
|
|
532
583
|
ReservedColumnKey,
|
|
533
584
|
MappedCell,
|
|
@@ -540,6 +591,7 @@ import {
|
|
|
540
591
|
mergeCellClasses,
|
|
541
592
|
mergeHeaderClasses,
|
|
542
593
|
mergeRowClasses,
|
|
594
|
+
readResolvedTracks,
|
|
543
595
|
} from './utils';
|
|
544
596
|
import { useBbTableContextHost } from '../../composables/useBbTableContext';
|
|
545
597
|
import { useLogger } from '@/composables/useLogger';
|
|
@@ -586,7 +638,7 @@ const props = withDefaults(defineProps<InternalProps<T>>(), {
|
|
|
586
638
|
|
|
587
639
|
const emit = defineEmits<BbTableEvents>();
|
|
588
640
|
|
|
589
|
-
defineSlots<
|
|
641
|
+
const slots = defineSlots<
|
|
590
642
|
BbTableSlots<Item> &
|
|
591
643
|
Partial<
|
|
592
644
|
Record<
|
|
@@ -620,6 +672,12 @@ defineSlots<
|
|
|
620
672
|
toggleSort?: () => void;
|
|
621
673
|
}) => any
|
|
622
674
|
>
|
|
675
|
+
> &
|
|
676
|
+
Partial<
|
|
677
|
+
Record<
|
|
678
|
+
InferredHeaderAffixSlots,
|
|
679
|
+
(props: BbTableHeaderAffixSlotProps<Item>) => any
|
|
680
|
+
>
|
|
623
681
|
>
|
|
624
682
|
>();
|
|
625
683
|
|
|
@@ -646,6 +704,7 @@ type InternalProps<K extends string> = Omit<BbTableProps<Item>, 'columns'> & {
|
|
|
646
704
|
|
|
647
705
|
type InferredBodySlots = ColumnName<T>;
|
|
648
706
|
type InferredHeaderSlots = PrefixWith<ColumnName<T>, 'header:'>;
|
|
707
|
+
type InferredHeaderAffixSlots = `${InferredHeaderSlots}:${HeaderAffixSuffix}`;
|
|
649
708
|
|
|
650
709
|
if (props.multiple && !Array.isArray(props.modelValue)) {
|
|
651
710
|
const logger = useLogger();
|
|
@@ -667,6 +726,15 @@ const RESERVED_COLUMN_KEYS: readonly ReservedColumnKey[] = [
|
|
|
667
726
|
'select',
|
|
668
727
|
];
|
|
669
728
|
|
|
729
|
+
// Annotated with the exported union for the same reason as above.
|
|
730
|
+
const HEADER_AFFIX_SUFFIXES: readonly HeaderAffixSuffix[] = [
|
|
731
|
+
'prepend',
|
|
732
|
+
'append',
|
|
733
|
+
];
|
|
734
|
+
|
|
735
|
+
const isHeaderAffixSuffix = (base: string): base is HeaderAffixSuffix =>
|
|
736
|
+
HEADER_AFFIX_SUFFIXES.includes(base as HeaderAffixSuffix);
|
|
737
|
+
|
|
670
738
|
if (process.env.NODE_ENV !== 'production') {
|
|
671
739
|
const reserved = props.columns.filter((column) =>
|
|
672
740
|
RESERVED_COLUMN_KEYS.includes(column.key as ReservedColumnKey)
|
|
@@ -679,17 +747,31 @@ if (process.env.NODE_ENV !== 'production') {
|
|
|
679
747
|
: 'Selection is controlled by the `selectable` prop.')
|
|
680
748
|
);
|
|
681
749
|
}
|
|
750
|
+
|
|
751
|
+
// Such a key would make the column's full-replacement slot name
|
|
752
|
+
// (`header:prepend`) collide with the table-wide affix slot of the same
|
|
753
|
+
// name, so the table-wide interpretation wins for it.
|
|
754
|
+
for (const column of props.columns) {
|
|
755
|
+
const base = columnKeyToSlotName(column.key);
|
|
756
|
+
if (isHeaderAffixSuffix(base)) {
|
|
757
|
+
useLogger().warn(
|
|
758
|
+
`BbTable: column key '${String(column.key)}' normalizes to the slot name '${base}', which collides with the table-wide \`#header:${base}\` slot. That name keeps its table-wide meaning and per-column header slots are unavailable for this column.`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
682
762
|
}
|
|
683
763
|
|
|
684
764
|
const { t } = useLocale();
|
|
685
765
|
|
|
686
|
-
|
|
766
|
+
/** The grid root (`.bb-table__table`, `role="table"`). */
|
|
767
|
+
const tableRef = ref<HTMLElement>();
|
|
687
768
|
const containerRef = ref<HTMLElement>();
|
|
688
769
|
|
|
689
770
|
const {
|
|
690
771
|
id: tableId,
|
|
691
772
|
parentId,
|
|
692
773
|
parentNode,
|
|
774
|
+
node,
|
|
693
775
|
cssVars,
|
|
694
776
|
parentVar,
|
|
695
777
|
setTracks,
|
|
@@ -702,6 +784,9 @@ const {
|
|
|
702
784
|
: undefined,
|
|
703
785
|
});
|
|
704
786
|
|
|
787
|
+
/** Id of the caption element the grid root is labelled by. */
|
|
788
|
+
const captionId = computed(() => `${tableId.value}__caption`);
|
|
789
|
+
|
|
705
790
|
/** The highlight as a 0-or-1 array, which is what the hash index wants. */
|
|
706
791
|
const highlightedValues = computed(() =>
|
|
707
792
|
isNil(highlighted.value) ? [] : [highlighted.value]
|
|
@@ -850,11 +935,11 @@ const inheritColumnWidths = computed(
|
|
|
850
935
|
);
|
|
851
936
|
|
|
852
937
|
/**
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
938
|
+
* A table that drives its widths — explicitly `fixed`, inheriting from a
|
|
939
|
+
* parent, or with a column carrying a `width` or `snap` (`snap` is a width
|
|
940
|
+
* definition: the parent track range). Under grid the tracks are authoritative
|
|
941
|
+
* in every layout, so this only feeds the `bb-table--fixed` class, kept as a
|
|
942
|
+
* styling hook for consumers.
|
|
858
943
|
*/
|
|
859
944
|
const fixedLayout = computed(
|
|
860
945
|
() =>
|
|
@@ -864,18 +949,16 @@ const fixedLayout = computed(
|
|
|
864
949
|
(inheritColumnWidths.value && !!parentId.value)
|
|
865
950
|
);
|
|
866
951
|
|
|
867
|
-
/**
|
|
868
|
-
* A nested table inheriting its parent's widths must size itself to the sum of
|
|
869
|
-
* those columns rather than stretching to fill its container — otherwise fixed
|
|
870
|
-
* layout would distribute the slack and the columns would no longer match the
|
|
871
|
-
* parent. Tables with the same column count already sum to the full width, so
|
|
872
|
-
* this is a no-op for them and only narrows tables with fewer columns.
|
|
873
|
-
*/
|
|
874
952
|
/** True for a nested table that inherits its column widths from a parent. */
|
|
875
953
|
const isInheriting = computed(
|
|
876
954
|
() => inheritColumnWidths.value && !!parentId.value
|
|
877
955
|
);
|
|
878
956
|
|
|
957
|
+
/**
|
|
958
|
+
* A nested table inheriting its parent's widths sizes itself to those tracks
|
|
959
|
+
* and shares the ancestor scrollport instead of opening its own — see
|
|
960
|
+
* `.bb-table--inherit-widths` in the CSS.
|
|
961
|
+
*/
|
|
879
962
|
const sizeToColumns = isInheriting;
|
|
880
963
|
|
|
881
964
|
/** Numbers and bare numeric strings (e.g. `400`, `'400'`) default to px; values
|
|
@@ -1021,117 +1104,219 @@ const fillColumnIndex = computed(() => {
|
|
|
1021
1104
|
// render, or columns skipped by the first snap) and those right of the last are
|
|
1022
1105
|
// folded into the edge columns as the structural offset, keeping the table
|
|
1023
1106
|
// aligned to the parent. `offsetStartValue` becomes the first column's padding.
|
|
1024
|
-
|
|
1107
|
+
/*
|
|
1108
|
+
* Nested geometry — the decision tree (plans/BBTABLE-GRID.md § v2.2).
|
|
1109
|
+
*
|
|
1110
|
+
* Every table publishes three kinds of rail: `select` = the START REGION
|
|
1111
|
+
* (everything left of the first content column), `0…N-1` = the pure content
|
|
1112
|
+
* columns, `actions` = the END REGION (unmapped trailing parent columns plus
|
|
1113
|
+
* the parent's actions). An inheriting table reserves both regions and snaps
|
|
1114
|
+
* its content columns 1:1, so alignment holds at any depth by induction.
|
|
1115
|
+
*
|
|
1116
|
+
* Per side, with R = the parent's region on that side:
|
|
1117
|
+
* 1. own structural column on that side → its track is `minmax(R, max-content)`:
|
|
1118
|
+
* equal to the parent's when the content fits, wider only when it must.
|
|
1119
|
+
* No padding fold; the region is consumed by the structural track.
|
|
1120
|
+
* 2. no own structural column, R > 0 → R is folded into the edge content
|
|
1121
|
+
* cell (padding + width, routable to the container via `--offset-*`) and
|
|
1122
|
+
* published UNFOLDED, with R itself published under the structural key,
|
|
1123
|
+
* so the next level reserves it too.
|
|
1124
|
+
* 3. no own structural column, R = 0 → nothing to reserve.
|
|
1125
|
+
* Surplus — an own structural cell wider than R (a select the parent lacks,
|
|
1126
|
+
* three buttons against the parent's two) — is reclaimed from the ADJACENT
|
|
1127
|
+
* content column only: its track shrinks by the surplus (a negative fold)
|
|
1128
|
+
* and its content edge moves by that much; every other rail holds. Interior
|
|
1129
|
+
* rails never absorb structure.
|
|
1130
|
+
*/
|
|
1131
|
+
|
|
1132
|
+
/** The parent's start region: its select cell plus any columns skipped by the first snap. */
|
|
1133
|
+
const startRegion = computed(() => {
|
|
1025
1134
|
const first = columnIntervals.value[0];
|
|
1026
|
-
// No parent to align to (a root table) means no offset at all — without this
|
|
1027
|
-
// the structural reclaim below would still fold a negative `-ownSelect` in.
|
|
1028
1135
|
if (!inheritColumnWidths.value || !parentId.value || !first) return null;
|
|
1029
|
-
|
|
1030
|
-
// column — the parent's select cell, if any, plus any skipped data columns.
|
|
1031
|
-
const head = gridRangeWidth(0, first.start);
|
|
1032
|
-
// This table's own select column sits flush at the left edge and takes its
|
|
1033
|
-
// measured width, so reclaim it from the fill (mirror of the actions side):
|
|
1034
|
-
// the first data column then lines the *next* column up flush with the
|
|
1035
|
-
// parent. When the parent has a select cell it is part of `head`, so only the
|
|
1036
|
-
// surplus over it is reclaimed; when the parent has none the whole select
|
|
1037
|
-
// width comes out of the first column, which shrinks to make room for it.
|
|
1038
|
-
const ownSelect = props.selectable ? structuralFit.select : null;
|
|
1039
|
-
if (!ownSelect) return head;
|
|
1040
|
-
return `calc(${head ?? '0px'} - ${ownSelect})`;
|
|
1136
|
+
return gridRangeWidth(0, first.start);
|
|
1041
1137
|
});
|
|
1042
|
-
|
|
1138
|
+
/**
|
|
1139
|
+
* The parent's end region: unmapped trailing data columns plus its actions
|
|
1140
|
+
* cell. With more columns than the parent there is no trailing region — the
|
|
1141
|
+
* table runs past the parent's edge with content-sized overflow columns.
|
|
1142
|
+
*/
|
|
1143
|
+
const endRegion = computed(() => {
|
|
1043
1144
|
const intervals = columnIntervals.value;
|
|
1044
1145
|
const last = intervals[intervals.length - 1];
|
|
1045
|
-
// No parent to align to (a root table) means no offset at all — without this
|
|
1046
|
-
// the structural reclaim below would still fold a negative `-ownActions` in.
|
|
1047
1146
|
if (!inheritColumnWidths.value || !parentId.value || !last) return null;
|
|
1048
|
-
// With more columns than the parent, the table runs past the parent's right
|
|
1049
|
-
// edge: there is no trailing parent region to fold, and the last column is a
|
|
1050
|
-
// content-sized overflow column, so there is no end fill.
|
|
1051
1147
|
if (overflowsParent.value) return null;
|
|
1052
|
-
|
|
1053
|
-
// trailing region via fixed layout, so folding the offset in too would double
|
|
1054
|
-
// it — and skew its right-aligned content inward.
|
|
1055
|
-
if (fillColumnIndex.value === props.columns.length - 1) return null;
|
|
1056
|
-
// The fill runs from the last data column's end all the way to the parent's
|
|
1057
|
-
// right edge — any unmapped data columns plus the parent's actions cell, if
|
|
1058
|
-
// it has one.
|
|
1059
|
-
const tail = gridRangeWidth(last.end, tableEndCell.value);
|
|
1060
|
-
// This table's own actions column sits flush against that right edge and
|
|
1061
|
-
// takes its measured content width, so reclaim it from the fill: the table
|
|
1062
|
-
// then ends exactly at the parent's edge instead of overflowing by the
|
|
1063
|
-
// actions width. When the parent has an actions cell it is part of `tail`, so
|
|
1064
|
-
// only the surplus over it is effectively reclaimed; when the parent has none
|
|
1065
|
-
// the whole actions width comes out of the data fill. The result may be
|
|
1066
|
-
// negative — the last data column then shrinks to make room for the actions.
|
|
1067
|
-
const ownActions = props.actions ? structuralFit.actions : null;
|
|
1068
|
-
if (!ownActions) return tail;
|
|
1069
|
-
return `calc(${tail ?? '0px'} - ${ownActions})`;
|
|
1148
|
+
return gridRangeWidth(last.end, tableEndCell.value);
|
|
1070
1149
|
});
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* This table's OWN last-measured structural track (`select` / `actions`) as
|
|
1153
|
+
* the px the grid resolved it to — `null` before the first measure. Feeds the
|
|
1154
|
+
* surplus reclaim: it is `minmax(R, max-content)`, independent of the data
|
|
1155
|
+
* columns, so folding it converges instead of oscillating.
|
|
1156
|
+
*/
|
|
1157
|
+
const ownStructuralTrack = (key: 'select' | 'actions') =>
|
|
1158
|
+
node.tracks[key] ?? null;
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* The edge fold. Without an own structural column it is the whole parent
|
|
1162
|
+
* region (case 2: padding + width on the edge content cell). With one, the
|
|
1163
|
+
* region lives in the structural track and only the SURPLUS over it — the
|
|
1164
|
+
* own cell's resolved width minus R, never positive-padded, ≤ 0 by
|
|
1165
|
+
* construction — is folded, shrinking the adjacent content column.
|
|
1166
|
+
*/
|
|
1167
|
+
const edgeFold = (
|
|
1168
|
+
region: string | null,
|
|
1169
|
+
own: string | null,
|
|
1170
|
+
hasOwn: boolean
|
|
1171
|
+
): string | null => {
|
|
1172
|
+
if (!inheritColumnWidths.value || !parentId.value) return null;
|
|
1173
|
+
if (!hasOwn) return region;
|
|
1174
|
+
if (!own) return null; // not measured yet — nothing to reclaim
|
|
1175
|
+
return `calc(${region ?? '0px'} - ${own})`;
|
|
1176
|
+
};
|
|
1177
|
+
const offsetStartValue = computed(() =>
|
|
1178
|
+
edgeFold(
|
|
1179
|
+
startRegion.value,
|
|
1180
|
+
props.selectable ? ownStructuralTrack('select') : null,
|
|
1181
|
+
!!props.selectable
|
|
1182
|
+
)
|
|
1183
|
+
);
|
|
1184
|
+
const offsetEndValue = computed(() =>
|
|
1185
|
+
overflowsParent.value
|
|
1186
|
+
? null
|
|
1187
|
+
: edgeFold(
|
|
1188
|
+
endRegion.value,
|
|
1189
|
+
props.actions ? ownStructuralTrack('actions') : null,
|
|
1190
|
+
!!props.actions
|
|
1191
|
+
)
|
|
1192
|
+
);
|
|
1071
1193
|
const offsetStartActive = computed(() => offsetStartValue.value !== null);
|
|
1072
1194
|
const offsetEndActive = computed(() => offsetEndValue.value !== null);
|
|
1073
1195
|
|
|
1074
1196
|
/**
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
1077
|
-
*
|
|
1197
|
+
* A structural track: `max-content` for a root table; for an inheriting table
|
|
1198
|
+
* with a parent region on that side, `minmax(R, max-content)` (case 1). The
|
|
1199
|
+
* chain is only emitted once the parent has published (see
|
|
1200
|
+
* `parentTracksPublished`).
|
|
1078
1201
|
*/
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1202
|
+
const structuralTrack = (region: string | null) =>
|
|
1203
|
+
region && parentTracksPublished.value
|
|
1204
|
+
? `minmax(${region}, max-content)`
|
|
1205
|
+
: 'max-content';
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* Whether the parent has published its tracks yet. Until it has (SSR, the
|
|
1209
|
+
* first client frame — a nested table mounts before its parent measures),
|
|
1210
|
+
* the inherited `var()` chains must NOT be emitted: their `0px` fallbacks
|
|
1211
|
+
* would crush the tracks to literally zero under grid (fixed table layout
|
|
1212
|
+
* used to redistribute; grid honours 0), and a fallbackless `var()` would
|
|
1213
|
+
* invalidate the whole template. Content-sized tracks stand in until the
|
|
1214
|
+
* parent's first measure lands, then the chains take over and this table's
|
|
1215
|
+
* own header-cell observer re-measures.
|
|
1216
|
+
*/
|
|
1217
|
+
const parentTracksPublished = computed(
|
|
1218
|
+
() => Object.keys(parentNode.value?.tracks ?? {}).length > 0
|
|
1219
|
+
);
|
|
1083
1220
|
|
|
1084
1221
|
/**
|
|
1085
1222
|
* Rendered columns in DOM order, each with the track key it publishes/inherits
|
|
1086
|
-
* and
|
|
1087
|
-
*
|
|
1088
|
-
*
|
|
1223
|
+
* and its `grid-template-columns` entry. The tracks ARE the column API: the
|
|
1224
|
+
* grid root owns the template and every rowgroup/row is a subgrid of it.
|
|
1225
|
+
*
|
|
1226
|
+
* - structural `select` / `actions`: `max-content` — min == max for both
|
|
1227
|
+
* cells (the actions cell is `white-space: nowrap`, which is what makes that
|
|
1228
|
+
* track sizing-critical).
|
|
1229
|
+
* - frozen (`width`): the declared length verbatim. A `%` resolves against the
|
|
1230
|
+
* root box, which under `width: fit-content; min-width: 100%` matches the
|
|
1231
|
+
* old `<table>` box. When EVERY data column of a root table is frozen there
|
|
1232
|
+
* is no flexible column to absorb the container's surplus, so each becomes
|
|
1233
|
+
* `minmax(<width>, <n>fr)` with `n` = its numeric width: the declared width
|
|
1234
|
+
* is the floor, and any surplus is shared in proportion to the declared
|
|
1235
|
+
* widths — the table keeps filling its container instead of leaving a
|
|
1236
|
+
* trailing gap (fixed table layout used to inflate those columns too).
|
|
1237
|
+
* - root unfrozen column: `auto` (min-content floor, max-content ceiling, an
|
|
1238
|
+
* equal share of the surplus) — the same wrap floor and overflow threshold as
|
|
1239
|
+
* table auto layout. With `fixed` every unfrozen column is `minmax(0, 1fr)`
|
|
1240
|
+
* instead: an equal split of the root, exactly as fixed table layout did.
|
|
1241
|
+
* - inheriting fill column: `minmax(0, 1fr)` — absorbs exactly the parent's
|
|
1242
|
+
* leftover and yields width authority when squeezed, like the old fixed
|
|
1243
|
+
* layout `auto` fill.
|
|
1244
|
+
* - inheriting overflow column (past the parent's last column): `min-content`,
|
|
1245
|
+
* what the deleted probe measured.
|
|
1246
|
+
* - inheriting in-range column: the parent's track `var()` chain (see
|
|
1247
|
+
* `gridRangeWidth`), gated on the parent having published.
|
|
1248
|
+
*
|
|
1249
|
+
* The first/last column additionally absorbs the structural edge offset. Never
|
|
1250
|
+
* emit `repeat()` or line names: the track reader splits a flat px list.
|
|
1089
1251
|
*/
|
|
1252
|
+
/**
|
|
1253
|
+
* A column `width` that is a grid sizing keyword rather than a length —
|
|
1254
|
+
* `toLength()` passes these through verbatim. They size the track directly
|
|
1255
|
+
* and must never be wrapped in `calc()`/`minmax()`.
|
|
1256
|
+
*/
|
|
1257
|
+
const isTrackKeyword = (value: string): boolean =>
|
|
1258
|
+
/^(auto|min-content|max-content|fit-content\(.*\)|-?[\d.]+fr)$/.test(
|
|
1259
|
+
value.trim()
|
|
1260
|
+
);
|
|
1261
|
+
|
|
1090
1262
|
const columnTracks = computed(() => {
|
|
1091
|
-
const sync = (key: string) =>
|
|
1092
|
-
inheritColumnWidths.value ? parentVar(key) : 'auto';
|
|
1093
|
-
// In fixed layout `auto` columns share width equally, so pin the structural
|
|
1094
|
-
// columns to their measured content width and let the data columns share.
|
|
1095
|
-
const structural = (key: 'select' | 'actions') =>
|
|
1096
|
-
fixedLayout.value && structuralFit[key]
|
|
1097
|
-
? (structuralFit[key] as string)
|
|
1098
|
-
: sync(key);
|
|
1099
1263
|
const tracks: Array<{ key: string; width: string }> = [];
|
|
1100
1264
|
|
|
1101
1265
|
if (props.selectable) {
|
|
1102
|
-
tracks.push({ key: 'select', width:
|
|
1266
|
+
tracks.push({ key: 'select', width: structuralTrack(startRegion.value) });
|
|
1103
1267
|
}
|
|
1104
1268
|
|
|
1105
1269
|
const lastIndex = props.columns.length - 1;
|
|
1270
|
+
// A root table whose data columns are ALL frozen (see the doc comment):
|
|
1271
|
+
// the frozen lengths become proportional floors instead of hard sizes.
|
|
1272
|
+
const allFrozen =
|
|
1273
|
+
!isInheriting.value &&
|
|
1274
|
+
props.columns.length > 0 &&
|
|
1275
|
+
props.columns.every((column) => column.width != null);
|
|
1106
1276
|
props.columns.forEach((column, index) => {
|
|
1107
1277
|
const key = String(index);
|
|
1108
1278
|
const frozen = column.width != null ? toLength(column.width) : null;
|
|
1109
1279
|
const interval = columnIntervals.value[index];
|
|
1110
1280
|
|
|
1111
|
-
//
|
|
1112
|
-
//
|
|
1113
|
-
// track
|
|
1114
|
-
//
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1281
|
+
// `span` is a CSS *length* expression (frozen width or inherited chain)
|
|
1282
|
+
// when the offset must be folded into it arithmetically; a `keyword`
|
|
1283
|
+
// track sizes to the cell's content, which already includes the padding
|
|
1284
|
+
// the offset adds, so it needs no folding.
|
|
1285
|
+
let span: string | null = null;
|
|
1286
|
+
let keyword: string | null = null;
|
|
1287
|
+
if (frozen != null && isTrackKeyword(frozen)) {
|
|
1288
|
+
// A keyword `width` (`auto`, `min-content`, `1fr`, …) is a track, not a
|
|
1289
|
+
// length: it can neither be folded into the offset `calc()` (invalid
|
|
1290
|
+
// at computed-value time — and an invalid template drops EVERY track)
|
|
1291
|
+
// nor serve as a `minmax()` floor with an `fr` share.
|
|
1292
|
+
keyword = frozen;
|
|
1293
|
+
} else if (frozen != null && allFrozen) {
|
|
1294
|
+
// `parseFloat` reads the leading number of any unit (`120px` → 120,
|
|
1295
|
+
// `25%` → 25); a non-numeric length still gets an equal share.
|
|
1296
|
+
const share = parseFloat(frozen) || 1;
|
|
1297
|
+
keyword = `minmax(${frozen}, ${share}fr)`;
|
|
1298
|
+
} else if (frozen != null) {
|
|
1122
1299
|
span = frozen;
|
|
1123
1300
|
} else if (isFlexColumn(index)) {
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
// the
|
|
1127
|
-
|
|
1301
|
+
keyword = props.fixed ? 'minmax(0, 1fr)' : 'auto';
|
|
1302
|
+
} else if (!parentTracksPublished.value) {
|
|
1303
|
+
// Inheriting, but the parent has not published yet (SSR, first client
|
|
1304
|
+
// frame): content-size on `max-content`. Checked BEFORE the overflow
|
|
1305
|
+
// test, which is vacuously true while the parent's geometry is unknown.
|
|
1306
|
+
keyword = 'max-content';
|
|
1128
1307
|
} else if (isOverflowColumn(interval)) {
|
|
1129
1308
|
// Inheriting table, past the parent's last column: no track to snap to,
|
|
1130
1309
|
// so size to its own content and let the table overflow (and scroll).
|
|
1131
|
-
|
|
1310
|
+
keyword = 'min-content';
|
|
1132
1311
|
} else {
|
|
1133
1312
|
// In range: snap to the parent's matching column.
|
|
1134
1313
|
span = gridRangeWidth(interval.start, interval.end);
|
|
1314
|
+
if (span === null) keyword = 'auto';
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
if (keyword !== null) {
|
|
1318
|
+
tracks.push({ key, width: keyword });
|
|
1319
|
+
return;
|
|
1135
1320
|
}
|
|
1136
1321
|
|
|
1137
1322
|
const offsets: string[] = [];
|
|
@@ -1142,22 +1327,35 @@ const columnTracks = computed(() => {
|
|
|
1142
1327
|
offsets.push('var(--offset-internal-end, 0px)');
|
|
1143
1328
|
}
|
|
1144
1329
|
|
|
1145
|
-
//
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1330
|
+
// Fold the offset(s) in so the track = offset(s) + the column's own span.
|
|
1331
|
+
const folded = offsets.length
|
|
1332
|
+
? `calc(${[...offsets, span].join(' + ')})`
|
|
1333
|
+
: (span as string);
|
|
1334
|
+
|
|
1335
|
+
// The fill column: the last unfrozen/unsnapped column of an inheriting
|
|
1336
|
+
// table grows to re-align everything after a narrower frozen sibling. Its
|
|
1337
|
+
// rail is the floor — it never shrinks below it (structural surplus is
|
|
1338
|
+
// not its job any more; see the decision tree above).
|
|
1339
|
+
const width =
|
|
1340
|
+
index === fillColumnIndex.value && !overflowsParent.value
|
|
1341
|
+
? `minmax(${folded}, 1fr)`
|
|
1342
|
+
: folded;
|
|
1150
1343
|
|
|
1151
1344
|
tracks.push({ key, width });
|
|
1152
1345
|
});
|
|
1153
1346
|
|
|
1154
1347
|
if (props.actions) {
|
|
1155
|
-
tracks.push({ key: 'actions', width:
|
|
1348
|
+
tracks.push({ key: 'actions', width: structuralTrack(endRegion.value) });
|
|
1156
1349
|
}
|
|
1157
1350
|
|
|
1158
1351
|
return tracks;
|
|
1159
1352
|
});
|
|
1160
1353
|
|
|
1354
|
+
/** The grid root's `grid-template-columns` value. */
|
|
1355
|
+
const gridTemplate = computed(() =>
|
|
1356
|
+
columnTracks.value.map((track) => track.width).join(' ')
|
|
1357
|
+
);
|
|
1358
|
+
|
|
1161
1359
|
/**
|
|
1162
1360
|
* Standalone edge-offset custom properties consumed by the first/last cell
|
|
1163
1361
|
* padding and the first/last column width. Set only when an offset is active;
|
|
@@ -1174,460 +1372,124 @@ const structuralOffset = computed<Record<string, string>>(() => {
|
|
|
1174
1372
|
return style;
|
|
1175
1373
|
});
|
|
1176
1374
|
|
|
1177
|
-
const containerStyle = computed(() => {
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
};
|
|
1182
|
-
|
|
1183
|
-
if (naturalTotalWidth.value) {
|
|
1184
|
-
vars['--natural-width'] = naturalTotalWidth.value;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
if (fillValue.value > 0) {
|
|
1188
|
-
vars['--fill'] = `${fillValue.value}px`;
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
return vars;
|
|
1192
|
-
});
|
|
1193
|
-
|
|
1194
|
-
/** Reads the header cells in render order: [select?, ...data, actions?]. */
|
|
1195
|
-
const headerCells = () =>
|
|
1196
|
-
Array.from(
|
|
1197
|
-
tableRef.value?.querySelectorAll<HTMLElement>(
|
|
1198
|
-
':scope > thead > tr.bb-table-header-row > th'
|
|
1199
|
-
) ?? []
|
|
1200
|
-
);
|
|
1201
|
-
|
|
1202
|
-
/** Measures the rendered columns and publishes them as this table's tracks. */
|
|
1203
|
-
const measureTracks = () => {
|
|
1204
|
-
const keys = columnTracks.value.map((track) => track.key);
|
|
1205
|
-
const tracks: Record<string, string> = {};
|
|
1206
|
-
headerCells().forEach((cell, index) => {
|
|
1207
|
-
const key = keys[index];
|
|
1208
|
-
if (key != null) tracks[key] = `${cell.getBoundingClientRect().width}px`;
|
|
1209
|
-
});
|
|
1210
|
-
setTracks(tracks);
|
|
1211
|
-
};
|
|
1375
|
+
const containerStyle = computed(() => ({
|
|
1376
|
+
...cssVars.value,
|
|
1377
|
+
...structuralOffset.value,
|
|
1378
|
+
}));
|
|
1212
1379
|
|
|
1213
1380
|
/**
|
|
1214
|
-
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1381
|
+
* The cells whose border boxes ARE this table's tracks: the header row's
|
|
1382
|
+
* cells, which are stretched subgrid items. When a `#thead` slot replaces the
|
|
1383
|
+
* header with fewer cells than tracks, the first row (head or body) with at
|
|
1384
|
+
* least one child per track stands in — so a short custom header on an empty
|
|
1385
|
+
* table publishes nothing until it has rows (see `observeColumns`), and the
|
|
1386
|
+
* `mappedItems` watch re-arms the observer when rows arrive or are re-keyed.
|
|
1387
|
+
* Scoped with `:scope >` so a nested table's rows are never picked up.
|
|
1219
1388
|
*/
|
|
1220
|
-
const
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
//
|
|
1228
|
-
//
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
'IMG',
|
|
1235
|
-
'svg',
|
|
1236
|
-
'VIDEO',
|
|
1237
|
-
'CANVAS',
|
|
1238
|
-
];
|
|
1239
|
-
|
|
1240
|
-
/**
|
|
1241
|
-
* Intrinsic occupied width of an element's in-flow contents: the sum of each
|
|
1242
|
-
* child's own intrinsic width plus horizontal margins and any flex/grid
|
|
1243
|
-
* column gaps — an actions cell holds several side-by-side controls and the
|
|
1244
|
-
* column must fit them all. Crucially this is *not* their shared extent: the
|
|
1245
|
-
* extent depends on how the container spreads them across its width, which
|
|
1246
|
-
* feeds the column width back into its own measurement and runs away into
|
|
1247
|
-
* the thousands.
|
|
1248
|
-
*
|
|
1249
|
-
* A shrink-wrapping child's rendered width does not depend on the cell, so
|
|
1250
|
-
* it is used as-is. A *stretching* child's rendered width IS its container's
|
|
1251
|
-
* width — a consumer slotting `<div class="flex">…</div>` would otherwise
|
|
1252
|
-
* ratchet the column wider on every container resize — so recurse into its
|
|
1253
|
-
* contents instead. A child stretches only in block formatting: flex/grid
|
|
1254
|
-
* items size to their content on the main axis, so inside a flex/grid
|
|
1255
|
-
* container every child's rendered width is trusted. Out-of-flow children
|
|
1256
|
-
* (e.g. an `sr-only` label parked far off-screen, a popover) are skipped so
|
|
1257
|
-
* they cannot inflate the result.
|
|
1258
|
-
*/
|
|
1259
|
-
const contentsWidth = (el: HTMLElement): number => {
|
|
1260
|
-
const style = getComputedStyle(el);
|
|
1261
|
-
const itemsShrinkWrap =
|
|
1262
|
-
style.display.includes('flex') || style.display.includes('grid');
|
|
1263
|
-
let total = 0;
|
|
1264
|
-
let count = 0;
|
|
1265
|
-
el.querySelectorAll<HTMLElement>(':scope > *').forEach((child) => {
|
|
1266
|
-
const childStyle = getComputedStyle(child);
|
|
1267
|
-
if (childStyle.position === 'absolute' || childStyle.position === 'fixed')
|
|
1268
|
-
return;
|
|
1269
|
-
const margin =
|
|
1270
|
-
parseFloat(childStyle.marginLeft) + parseFloat(childStyle.marginRight);
|
|
1271
|
-
const stretches =
|
|
1272
|
-
!itemsShrinkWrap &&
|
|
1273
|
-
!SHRINK_WRAPPING_TAGS.includes(child.tagName) &&
|
|
1274
|
-
childStyle.cssFloat === 'none' &&
|
|
1275
|
-
STRETCHING.includes(childStyle.display);
|
|
1276
|
-
const own = stretches
|
|
1277
|
-
? contentsWidth(child) +
|
|
1278
|
-
parseFloat(childStyle.paddingLeft) +
|
|
1279
|
-
parseFloat(childStyle.paddingRight) +
|
|
1280
|
-
parseFloat(childStyle.borderLeftWidth) +
|
|
1281
|
-
parseFloat(childStyle.borderRightWidth)
|
|
1282
|
-
: child.getBoundingClientRect().width;
|
|
1283
|
-
total += own + (margin || 0);
|
|
1284
|
-
count += 1;
|
|
1285
|
-
});
|
|
1286
|
-
if (count === 0) {
|
|
1287
|
-
// Text-only content: measure the laid-out text run itself.
|
|
1288
|
-
const range = document.createRange();
|
|
1289
|
-
range.selectNodeContents(el);
|
|
1290
|
-
return range.getBoundingClientRect().width;
|
|
1291
|
-
}
|
|
1292
|
-
if (count > 1) {
|
|
1293
|
-
const gap = parseFloat(style.columnGap);
|
|
1294
|
-
if (gap) total += gap * (count - 1);
|
|
1295
|
-
}
|
|
1296
|
-
return total;
|
|
1297
|
-
};
|
|
1298
|
-
|
|
1299
|
-
const fit = (selector: string) => {
|
|
1300
|
-
let max = 0;
|
|
1301
|
-
table.querySelectorAll<HTMLElement>(selector).forEach((cell) => {
|
|
1302
|
-
const style = getComputedStyle(cell);
|
|
1303
|
-
const padding =
|
|
1304
|
-
parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
|
|
1305
|
-
max = Math.max(max, contentsWidth(cell) + padding);
|
|
1306
|
-
});
|
|
1307
|
-
return max > 0 ? `${Math.ceil(max)}px` : null;
|
|
1308
|
-
};
|
|
1309
|
-
|
|
1310
|
-
// Scoped to this table's own header/body rows: an unscoped descendant query
|
|
1311
|
-
// would also match the structural cells of a nested table living in an expand
|
|
1312
|
-
// row, letting a child's actions/select width hijack this table's column.
|
|
1313
|
-
structuralFit.select = props.selectable
|
|
1314
|
-
? fit(
|
|
1315
|
-
':scope > thead > tr > th.bb-table-header--select, :scope > tbody > tr > td.bb-table-data__cell--select'
|
|
1316
|
-
)
|
|
1317
|
-
: null;
|
|
1318
|
-
structuralFit.actions = props.actions
|
|
1319
|
-
? fit(
|
|
1320
|
-
':scope > thead > tr > th.bb-table-header--actions, :scope > tbody > tr > td.bb-table__cell--actions'
|
|
1389
|
+
const trackCells = (): HTMLElement[] => {
|
|
1390
|
+
const root = tableRef.value;
|
|
1391
|
+
const trackCount = columnTracks.value.length;
|
|
1392
|
+
if (!root || !trackCount) return [];
|
|
1393
|
+
const headerRow = root.querySelector<HTMLElement>(
|
|
1394
|
+
':scope > .bb-table__head > .bb-table-header-row'
|
|
1395
|
+
);
|
|
1396
|
+
// The default header row is the first candidate, never the only one: a
|
|
1397
|
+
// custom `#thead` may reuse its class with fewer cells than tracks.
|
|
1398
|
+
const candidates = [
|
|
1399
|
+
...(headerRow ? [headerRow] : []),
|
|
1400
|
+
...Array.from(
|
|
1401
|
+
root.querySelectorAll<HTMLElement>(
|
|
1402
|
+
':scope > .bb-table__head > [role="row"], :scope > .bb-table__body > [role="row"]'
|
|
1321
1403
|
)
|
|
1322
|
-
|
|
1404
|
+
),
|
|
1405
|
+
];
|
|
1406
|
+
const row = candidates.find((r) => r.children.length >= trackCount);
|
|
1407
|
+
return row ? (Array.from(row.children) as HTMLElement[]) : [];
|
|
1323
1408
|
};
|
|
1324
1409
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
measureTracks();
|
|
1328
|
-
};
|
|
1410
|
+
/** The joined px list last published, for the equality guard. */
|
|
1411
|
+
let publishedTracks = '';
|
|
1329
1412
|
|
|
1330
1413
|
/**
|
|
1331
|
-
*
|
|
1332
|
-
*
|
|
1414
|
+
* Measures the resolved columns and publishes them as this table's tracks.
|
|
1415
|
+
* ONE read — `getComputedStyle(root).gridTemplateColumns` — yields the used px
|
|
1416
|
+
* of every track type through both subgrid levels, synchronously and to the
|
|
1417
|
+
* subpixel. The px substrings are published verbatim (no float round-trip:
|
|
1418
|
+
* nested alignment is asserted at 1.5px and depends on it), zipped
|
|
1419
|
+
* positionally with `columnTracks`.
|
|
1333
1420
|
*
|
|
1334
|
-
*
|
|
1335
|
-
*
|
|
1336
|
-
*
|
|
1337
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
* `available` = containerWidth − frozenColumnWidths − structuralColumnWidths
|
|
1340
|
-
* and `minimumTotal` = sum of the auto columns' minimums.
|
|
1341
|
-
*
|
|
1342
|
-
* So when the container has room (`available ≥ minimumTotal`) the slack is shared
|
|
1343
|
-
* out proportionally to each column's minimum, scaling them up together; when it
|
|
1344
|
-
* does not, every column drops to its minimum and the table-level `min-width`
|
|
1345
|
-
* lets it overflow (the container scrolls) rather than squashing content further.
|
|
1346
|
-
*
|
|
1347
|
-
* Re-measured after any DOM mutation (debounced 200 ms) and after any container
|
|
1348
|
-
* resize (via a dedicated ResizeObserver on the container element).
|
|
1421
|
+
* Guarded twice: a non-px token means the table is hidden (the specified
|
|
1422
|
+
* template comes back, and the observer fires on hide) — keep the last good
|
|
1423
|
+
* vars, the observer fires again on re-show; and `setTracks` is skipped on an
|
|
1424
|
+
* unchanged list — loop safety, since publishing re-renders the container
|
|
1425
|
+
* style, which is observed by the very header cells that trigger this.
|
|
1349
1426
|
*/
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
const
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
*/
|
|
1364
|
-
const fillValue = ref(0);
|
|
1365
|
-
|
|
1366
|
-
/**
|
|
1367
|
-
* Total minimum width of the table (all columns at their `min-content`, plus the
|
|
1368
|
-
* structural columns). Published as `--natural-width` and used as
|
|
1369
|
-
* `min-width` on the `<table>` so the table scrolls horizontally only once the
|
|
1370
|
-
* container is narrower than this true minimum.
|
|
1371
|
-
*/
|
|
1372
|
-
const naturalTotalWidth = ref<string | null>(null);
|
|
1373
|
-
|
|
1374
|
-
const runNaturalWidthProbe = () => {
|
|
1375
|
-
const container = containerRef.value;
|
|
1376
|
-
if (!container || !fixedLayout.value) return;
|
|
1377
|
-
// Runs for a frozen-column table (to share space), or any inheriting table
|
|
1378
|
-
// (to content-size overflow columns and drive the fill column's slack).
|
|
1379
|
-
if (
|
|
1380
|
-
!props.columns.some((col) => col.width != null) &&
|
|
1381
|
-
!overflowsParent.value &&
|
|
1382
|
-
fillColumnIndex.value < 0
|
|
1383
|
-
)
|
|
1384
|
-
return;
|
|
1385
|
-
|
|
1386
|
-
const containerWidth = container.offsetWidth;
|
|
1387
|
-
const dataOffset = props.selectable ? 1 : 0;
|
|
1388
|
-
|
|
1389
|
-
// ── Real-table measurements ────────────────────────────────────────────
|
|
1390
|
-
// Structural column widths come from measureStructuralFit (already run).
|
|
1391
|
-
const selectWidth = structuralFit.select
|
|
1392
|
-
? parseFloat(structuralFit.select)
|
|
1393
|
-
: 0;
|
|
1394
|
-
const actionsWidth = structuralFit.actions
|
|
1395
|
-
? parseFloat(structuralFit.actions)
|
|
1396
|
-
: 0;
|
|
1397
|
-
|
|
1398
|
-
// Live rendered widths from the fixed-layout table. `frozenTotal` (declared
|
|
1399
|
-
// widths) feeds the stable min-width floor; `nonFlexTotal` (frozen + inherited)
|
|
1400
|
-
// is what the flex columns share the remainder of. The inherited part is only
|
|
1401
|
-
// used for that `available` figure — never fed back into min-width, since those
|
|
1402
|
-
// widths stretch with the table and would otherwise lock min-width above
|
|
1403
|
-
// max-content and force a spurious uniform stretch on every column.
|
|
1404
|
-
const realThs = headerCells();
|
|
1405
|
-
let frozenTotal = 0;
|
|
1406
|
-
let nonFlexTotal = 0;
|
|
1407
|
-
props.columns.forEach((col, i) => {
|
|
1408
|
-
const width = realThs[dataOffset + i]?.getBoundingClientRect().width ?? 0;
|
|
1409
|
-
if (col.width != null) frozenTotal += width;
|
|
1410
|
-
if (!isFlexColumn(i)) nonFlexTotal += width;
|
|
1411
|
-
});
|
|
1412
|
-
|
|
1413
|
-
// ── Offscreen probe for the auto columns' minimum (min-content) widths ──
|
|
1414
|
-
// The clone's inner table is forced to `width: min-content` and the auto <col>
|
|
1415
|
-
// hints are stripped so each <th> reports the narrowest width it can take
|
|
1416
|
-
// before content would be clipped, independent of the container size or
|
|
1417
|
-
// previous probe cycles.
|
|
1418
|
-
const probe = document.createElement('div');
|
|
1419
|
-
probe.style.cssText = [
|
|
1420
|
-
'position:absolute',
|
|
1421
|
-
'top:-9999px',
|
|
1422
|
-
'left:-9999px',
|
|
1423
|
-
'visibility:hidden',
|
|
1424
|
-
'pointer-events:none',
|
|
1425
|
-
`width:${containerWidth}px`,
|
|
1426
|
-
`height:${container.offsetHeight}px`,
|
|
1427
|
-
'overflow:visible',
|
|
1428
|
-
].join(';');
|
|
1429
|
-
|
|
1430
|
-
const clone = container.cloneNode(true) as HTMLElement;
|
|
1431
|
-
clone.classList.remove('bb-table--fixed');
|
|
1432
|
-
|
|
1433
|
-
// Expand rows are full-width (colspan) and hold nested tables; under the
|
|
1434
|
-
// auto-layout probe their content would back-pressure this table's own
|
|
1435
|
-
// columns, so drop them and measure this table's columns in isolation.
|
|
1436
|
-
clone
|
|
1437
|
-
.querySelectorAll('.bb-table-expand__row')
|
|
1438
|
-
.forEach((row) => row.remove());
|
|
1439
|
-
|
|
1440
|
-
// Auto <col> hints are cleared so a previous cycle's computed width cannot
|
|
1441
|
-
// act as a minimum and inflate the measurement; frozen hints are left intact
|
|
1442
|
-
// since their declared width is a genuine floor for that column.
|
|
1443
|
-
const probeColEls = Array.from(clone.querySelectorAll<HTMLElement>('col'));
|
|
1444
|
-
props.columns.forEach((col, i) => {
|
|
1445
|
-
if (col.width == null) {
|
|
1446
|
-
const probeCol = probeColEls[dataOffset + i];
|
|
1447
|
-
if (probeCol) probeCol.style.width = '';
|
|
1448
|
-
}
|
|
1427
|
+
const measureTracks = () => {
|
|
1428
|
+
const root = tableRef.value;
|
|
1429
|
+
if (!root) return;
|
|
1430
|
+
const resolved = readResolvedTracks(root);
|
|
1431
|
+
if (!resolved) return;
|
|
1432
|
+
const keys = columnTracks.value.map((track) => track.key);
|
|
1433
|
+
if (resolved.length !== keys.length) return;
|
|
1434
|
+
const joined = resolved.join(' ');
|
|
1435
|
+
if (joined === publishedTracks) return;
|
|
1436
|
+
publishedTracks = joined;
|
|
1437
|
+
const tracks: Record<string, string> = {};
|
|
1438
|
+
keys.forEach((key, index) => {
|
|
1439
|
+
tracks[key] = resolved[index];
|
|
1449
1440
|
});
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1441
|
+
// Publish the CONTRACT, not the raw template (decision tree, case 2): an
|
|
1442
|
+
// edge content column that folded a parent region into its width is
|
|
1443
|
+
// published unfolded — the pure content rail — and the region goes out
|
|
1444
|
+
// under the structural key even though no cell renders it, so the next
|
|
1445
|
+
// level reserves it exactly as this one did.
|
|
1446
|
+
const lastKey = String(props.columns.length - 1);
|
|
1447
|
+
const startFold = offsetStartValue.value;
|
|
1448
|
+
const endFold = offsetEndValue.value;
|
|
1449
|
+
if (startFold && tracks['0']) {
|
|
1450
|
+
tracks['0'] = `calc(${tracks['0']} - (${startFold}))`;
|
|
1451
|
+
if (!props.selectable && startRegion.value)
|
|
1452
|
+
tracks.select = startRegion.value;
|
|
1455
1453
|
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
)
|
|
1463
|
-
|
|
1464
|
-
? Array.from(headerRow.querySelectorAll<HTMLElement>(':scope > th'))
|
|
1465
|
-
: [];
|
|
1466
|
-
|
|
1467
|
-
// Min-content of every unfrozen column (0 for frozen). Independent of the live
|
|
1468
|
-
// table's width, so it is a stable basis for both the flex share and the
|
|
1469
|
-
// min-width floor — unlike the rendered inherited widths.
|
|
1470
|
-
const minContents = props.columns.map((col, i) => {
|
|
1471
|
-
if (col.width != null) return 0;
|
|
1472
|
-
const th = ths[dataOffset + i];
|
|
1473
|
-
return th ? th.getBoundingClientRect().width : 0;
|
|
1474
|
-
});
|
|
1475
|
-
|
|
1476
|
-
document.body.removeChild(probe);
|
|
1477
|
-
|
|
1478
|
-
// ── Proportional <col> widths ──────────────────────────────────────────
|
|
1479
|
-
// Only the flex columns share `available`; the inherited columns are already
|
|
1480
|
-
// laid out and excluded via `nonFlexTotal`.
|
|
1481
|
-
const flexMinTotal = props.columns.reduce(
|
|
1482
|
-
(s, _, i) => s + (isFlexColumn(i) ? minContents[i] : 0),
|
|
1483
|
-
0
|
|
1484
|
-
);
|
|
1485
|
-
const available = containerWidth - nonFlexTotal - selectWidth - actionsWidth;
|
|
1486
|
-
|
|
1487
|
-
// Share the available space proportionally to each flex column's minimum, but
|
|
1488
|
-
// never drop below that minimum — when space runs out the table overflows
|
|
1489
|
-
// (and the container scrolls) instead of clipping content.
|
|
1490
|
-
//
|
|
1491
|
-
// Widths are kept sub-pixel (not rounded up): the raw shares sum to exactly
|
|
1492
|
-
// `available`, whereas ceiling each column would over-allocate by up to a
|
|
1493
|
-
// pixel apiece and, under `table-layout: fixed`, push the table a few px past
|
|
1494
|
-
// the container and trip the horizontal scrollbar. `toFixed` only trims the
|
|
1495
|
-
// float tail, well below the 1px a scrollbar needs.
|
|
1496
|
-
const px = (n: number) => `${Number(n.toFixed(3))}px`;
|
|
1497
|
-
probeColWidths.value = props.columns.map((_, i) => {
|
|
1498
|
-
if (!isFlexColumn(i)) return null;
|
|
1499
|
-
const minimum = minContents[i];
|
|
1500
|
-
if (flexMinTotal <= 0) return px(minimum);
|
|
1501
|
-
const share = available > 0 ? available * (minimum / flexMinTotal) : 0;
|
|
1502
|
-
return px(Math.max(minimum, share));
|
|
1503
|
-
});
|
|
1504
|
-
|
|
1505
|
-
// Min-content per unfrozen column, for content-sizing an inheriting table's
|
|
1506
|
-
// overflow columns (no parent track to snap to).
|
|
1507
|
-
columnMinWidths.value = minContents.map((w) => (w > 0 ? px(w) : null));
|
|
1508
|
-
|
|
1509
|
-
// Leftover the inheriting fill column must absorb to reach the parent's width.
|
|
1510
|
-
// `parentTotal` is the parent table's full width (the expand cell this table
|
|
1511
|
-
// sits in); the container itself shrink-wraps to the table, so it can't be the
|
|
1512
|
-
// reference. Self-correcting: the live total already includes last cycle's
|
|
1513
|
-
// fill, so adding it back recovers the intrinsic total and the result is the
|
|
1514
|
-
// same once the table fills (0 when it already fills or overflows).
|
|
1515
|
-
if (fillColumnIndex.value >= 0) {
|
|
1516
|
-
const parentTotal = Object.values(parentNode.value?.tracks ?? {}).reduce(
|
|
1517
|
-
(s, w) => s + (parseFloat(w) || 0),
|
|
1518
|
-
0
|
|
1519
|
-
);
|
|
1520
|
-
const liveTotal = realThs.reduce(
|
|
1521
|
-
(s, th) => s + th.getBoundingClientRect().width,
|
|
1522
|
-
0
|
|
1523
|
-
);
|
|
1524
|
-
fillValue.value = Math.max(0, parentTotal - liveTotal + fillValue.value);
|
|
1525
|
-
} else {
|
|
1526
|
-
fillValue.value = 0;
|
|
1454
|
+
if (endFold && tracks[lastKey]) {
|
|
1455
|
+
tracks[lastKey] = `calc(${tracks[lastKey]} - (${endFold}))`;
|
|
1456
|
+
if (!props.actions && endRegion.value) tracks.actions = endRegion.value;
|
|
1457
|
+
}
|
|
1458
|
+
setTracks(tracks);
|
|
1459
|
+
// Sticky column offsets are prefix sums of these tracks: keep them current.
|
|
1460
|
+
if (props.fixedColumns?.length) {
|
|
1461
|
+
applyFixedTableColumns(root, props.fixedColumns);
|
|
1527
1462
|
}
|
|
1528
|
-
|
|
1529
|
-
// Table min-width = frozen (declared) widths + every unfrozen column's
|
|
1530
|
-
// min-content + structural. Built only from values that do not depend on the
|
|
1531
|
-
// table's own width, so it can never exceed max-content and force a stretch.
|
|
1532
|
-
const minContentTotal = minContents.reduce((s, w) => s + w, 0);
|
|
1533
|
-
const naturalTotal =
|
|
1534
|
-
frozenTotal + minContentTotal + selectWidth + actionsWidth;
|
|
1535
|
-
naturalTotalWidth.value =
|
|
1536
|
-
naturalTotal > 0 ? `${Math.ceil(naturalTotal)}px` : null;
|
|
1537
1463
|
};
|
|
1538
1464
|
|
|
1539
|
-
let naturalWidthDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
1540
|
-
let mutationObserver: MutationObserver | undefined;
|
|
1541
|
-
|
|
1542
1465
|
/**
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
1546
|
-
*
|
|
1547
|
-
*
|
|
1548
|
-
*
|
|
1466
|
+
* The single observer: the per-header-cell ResizeObserver. A header cell is a
|
|
1467
|
+
* stretched subgrid item whose border box is the track, so it is the complete
|
|
1468
|
+
* signal for every cause of a track change — including content-driven
|
|
1469
|
+
* redistribution at a constant root width, which a root observer cannot see.
|
|
1470
|
+
* Measuring inside the callback is post-layout, pre-paint: the republished
|
|
1471
|
+
* vars restyle nested children in the same frame.
|
|
1549
1472
|
*/
|
|
1550
|
-
const measuring = ref(false);
|
|
1551
|
-
|
|
1552
|
-
/** Whether the table genuinely overflowed its container at the last settle. */
|
|
1553
|
-
let hadOverflow = false;
|
|
1554
|
-
|
|
1555
|
-
const updateOverflowState = () => {
|
|
1556
|
-
const container = containerRef.value;
|
|
1557
|
-
const table = tableRef.value;
|
|
1558
|
-
hadOverflow =
|
|
1559
|
-
!!container &&
|
|
1560
|
-
!!table &&
|
|
1561
|
-
table.getBoundingClientRect().width > container.clientWidth + 1;
|
|
1562
|
-
};
|
|
1563
|
-
|
|
1564
|
-
/** Invalidates the cached natural width and schedules a fresh probe. */
|
|
1565
|
-
const scheduleNaturalWidthProbe = () => {
|
|
1566
|
-
if (naturalWidthDebounceTimer !== null)
|
|
1567
|
-
clearTimeout(naturalWidthDebounceTimer);
|
|
1568
|
-
naturalWidthDebounceTimer = setTimeout(() => {
|
|
1569
|
-
naturalWidthDebounceTimer = null;
|
|
1570
|
-
runNaturalWidthProbe();
|
|
1571
|
-
// Lift the overflow clip only after the new column widths have rendered,
|
|
1572
|
-
// then re-read the settled overflow state (a scrollbar reappears here
|
|
1573
|
-
// only if the table truly cannot fit the new container width).
|
|
1574
|
-
nextTick(() => {
|
|
1575
|
-
measuring.value = false;
|
|
1576
|
-
updateOverflowState();
|
|
1577
|
-
});
|
|
1578
|
-
}, 200);
|
|
1579
|
-
};
|
|
1580
|
-
|
|
1581
1473
|
let resizeObserver: ResizeObserver | undefined;
|
|
1582
|
-
/** Observes the container element specifically for size changes so the
|
|
1583
|
-
* proportional <col> widths are recomputed whenever the container grows or
|
|
1584
|
-
* shrinks — separately from the header-cell observer that drives measureTracks. */
|
|
1585
|
-
let containerResizeObserver: ResizeObserver | undefined;
|
|
1586
1474
|
|
|
1587
1475
|
const observeColumns = () => {
|
|
1588
1476
|
if (!resizeObserver) return;
|
|
1589
1477
|
resizeObserver.disconnect();
|
|
1590
|
-
|
|
1591
|
-
|
|
1478
|
+
const cells = trackCells();
|
|
1479
|
+
cells.forEach((cell) => resizeObserver?.observe(cell));
|
|
1480
|
+
// No observable cells (a short custom `#thead` over an empty table): publish
|
|
1481
|
+
// nothing rather than a snapshot nothing would ever refresh — an inheriting
|
|
1482
|
+
// child then stays on its content-sized gate until rows arrive.
|
|
1483
|
+
if (cells.length) measureTracks();
|
|
1592
1484
|
};
|
|
1593
1485
|
|
|
1594
1486
|
onMounted(() => {
|
|
1595
|
-
resizeObserver = new ResizeObserver(
|
|
1487
|
+
resizeObserver = new ResizeObserver(measureTracks);
|
|
1596
1488
|
observeColumns();
|
|
1597
|
-
|
|
1598
|
-
// Initial probe runs after the first render so all cells have content.
|
|
1599
|
-
nextTick(runNaturalWidthProbe);
|
|
1600
|
-
|
|
1601
|
-
// Invalidate the cache whenever rows or cell content change.
|
|
1602
|
-
if (tableRef.value) {
|
|
1603
|
-
mutationObserver = new MutationObserver(scheduleNaturalWidthProbe);
|
|
1604
|
-
mutationObserver.observe(tableRef.value, {
|
|
1605
|
-
childList: true,
|
|
1606
|
-
subtree: true,
|
|
1607
|
-
characterData: true,
|
|
1608
|
-
});
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
// Re-probe whenever the container itself is resized so that proportional
|
|
1612
|
-
// <col> widths stay accurate as the available space changes. Runs before
|
|
1613
|
-
// paint, so clipping overflow here means the stale-width scrollbar never
|
|
1614
|
-
// shows. The hadOverflow guard also breaks the feedback loop of the clip
|
|
1615
|
-
// itself resizing the content box.
|
|
1616
|
-
if (containerRef.value) {
|
|
1617
|
-
containerResizeObserver = new ResizeObserver(() => {
|
|
1618
|
-
if (!hadOverflow) measuring.value = true;
|
|
1619
|
-
scheduleNaturalWidthProbe();
|
|
1620
|
-
});
|
|
1621
|
-
containerResizeObserver.observe(containerRef.value);
|
|
1622
|
-
}
|
|
1623
1489
|
});
|
|
1624
1490
|
|
|
1625
1491
|
onBeforeUnmount(() => {
|
|
1626
1492
|
resizeObserver?.disconnect();
|
|
1627
|
-
containerResizeObserver?.disconnect();
|
|
1628
|
-
mutationObserver?.disconnect();
|
|
1629
|
-
if (naturalWidthDebounceTimer !== null)
|
|
1630
|
-
clearTimeout(naturalWidthDebounceTimer);
|
|
1631
1493
|
});
|
|
1632
1494
|
|
|
1633
1495
|
watch(columnTracks, () => nextTick(observeColumns), { flush: 'post' });
|
|
@@ -1658,8 +1520,8 @@ const loading = computed(() => props.loading || innerLoading.value);
|
|
|
1658
1520
|
* out: a refetch is about to replace the rows, so editing, selecting or sorting
|
|
1659
1521
|
* them races the incoming data.
|
|
1660
1522
|
*
|
|
1661
|
-
* Applied to
|
|
1662
|
-
* purpose — `inert` removes descendants from the a11y tree, and the loading
|
|
1523
|
+
* Applied to the header rowgroup and to each row rather than to the table or
|
|
1524
|
+
* the body rowgroup on purpose — `inert` removes descendants from the a11y tree, and the loading
|
|
1663
1525
|
* announcement lives in the same body as the rows, so a broader target would
|
|
1664
1526
|
* silence it and the table would go busy in complete silence.
|
|
1665
1527
|
*/
|
|
@@ -1688,7 +1550,6 @@ const containerClass = computed(() => ({
|
|
|
1688
1550
|
'bb-table--loading': loading.value,
|
|
1689
1551
|
'bb-table--empty': !options.value.length,
|
|
1690
1552
|
'bb-table--selectable': !!props.selectable,
|
|
1691
|
-
'bb-table--measuring': measuring.value,
|
|
1692
1553
|
'scrollbar-border': true,
|
|
1693
1554
|
}));
|
|
1694
1555
|
|
|
@@ -1856,7 +1717,14 @@ const mapColumnToCell = (
|
|
|
1856
1717
|
const mappedHeaders = computed(() =>
|
|
1857
1718
|
props.columns.map((column: BbTableColumn<Item>): MappedHeader => {
|
|
1858
1719
|
const align = column.align ?? 'left';
|
|
1859
|
-
const
|
|
1720
|
+
const base = slotNamesByKey.value[column.key];
|
|
1721
|
+
// A key normalizing to an affix suffix collides with the table-wide
|
|
1722
|
+
// `header:prepend/append` slots, and the table-wide reading wins (dev
|
|
1723
|
+
// builds warn) — so route this column's own slot to a name no template
|
|
1724
|
+
// can declare (the space guarantees it) and let the fallback render.
|
|
1725
|
+
const slotName = isHeaderAffixSuffix(base)
|
|
1726
|
+
? `header:${base} (shadowed)`
|
|
1727
|
+
: `header:${base}`;
|
|
1860
1728
|
// The live sort model wins over the static `sorted` column flag.
|
|
1861
1729
|
const sortOrder = sortOrderFor(column.key);
|
|
1862
1730
|
const sorted = sortOrder ?? column.sorted;
|
|
@@ -1883,6 +1751,39 @@ const mappedHeaders = computed(() =>
|
|
|
1883
1751
|
})
|
|
1884
1752
|
);
|
|
1885
1753
|
|
|
1754
|
+
/**
|
|
1755
|
+
* Resolves a header cell's prepend/append content: the column's own
|
|
1756
|
+
* `header:<key>:prepend/append` slot wins over the table-wide
|
|
1757
|
+
* `header:prepend/append` one — they never stack (BbDropdown's item rule).
|
|
1758
|
+
* A column whose key shadows an affix suffix only gets the table-wide slots.
|
|
1759
|
+
*/
|
|
1760
|
+
const headerAffixFor = (key: string, suffix: HeaderAffixSuffix) => {
|
|
1761
|
+
const base = slotNamesByKey.value[key];
|
|
1762
|
+
return (
|
|
1763
|
+
(isHeaderAffixSuffix(base)
|
|
1764
|
+
? undefined
|
|
1765
|
+
: slots[`header:${base}:${suffix}`]) ?? slots[`header:${suffix}`]
|
|
1766
|
+
);
|
|
1767
|
+
};
|
|
1768
|
+
|
|
1769
|
+
/**
|
|
1770
|
+
* Affix slots receive the `header:<key>` payload plus the column key so a
|
|
1771
|
+
* table-wide template can branch per column. Named `columnKey`, not `key`:
|
|
1772
|
+
* the payload is v-bound onto a rendered slot function, where `key` would be
|
|
1773
|
+
* consumed as the vnode key instead of reaching the slot.
|
|
1774
|
+
*/
|
|
1775
|
+
const headerAffixScope = (
|
|
1776
|
+
header: MappedHeader
|
|
1777
|
+
): BbTableHeaderAffixSlotProps<Item> => ({
|
|
1778
|
+
classes: header.classes,
|
|
1779
|
+
columnKey: header.key,
|
|
1780
|
+
items: internalItems.value,
|
|
1781
|
+
label: header.label,
|
|
1782
|
+
sortable: header.sortable,
|
|
1783
|
+
sortOrder: header.sortOrder,
|
|
1784
|
+
toggleSort: () => toggleSortFor(header.key),
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1886
1787
|
/**
|
|
1887
1788
|
* Resolves the CURRENT option for a row at event time. Row handlers are built
|
|
1888
1789
|
* in the selection-free formatter projection below, so they cannot capture a
|
|
@@ -1924,17 +1825,17 @@ const projectedItems = computed(() =>
|
|
|
1924
1825
|
const item = liveOptionFor(index, base.valueHash);
|
|
1925
1826
|
if (!item) return;
|
|
1926
1827
|
onRowClickHighlight(event, item);
|
|
1927
|
-
emit('click:row', event, item
|
|
1828
|
+
emit('click:row', event, item.item);
|
|
1928
1829
|
},
|
|
1929
1830
|
onRowContextMenu: (event: MouseEvent) => {
|
|
1930
1831
|
const item = liveOptionFor(index, base.valueHash);
|
|
1931
1832
|
if (!item) return;
|
|
1932
|
-
emit('contextmenu:row', event, item
|
|
1833
|
+
emit('contextmenu:row', event, item.item);
|
|
1933
1834
|
},
|
|
1934
1835
|
onRowDblClick: (event: MouseEvent) => {
|
|
1935
1836
|
const item = liveOptionFor(index, base.valueHash);
|
|
1936
1837
|
if (!item) return;
|
|
1937
|
-
emit('dblclick:row', event, item
|
|
1838
|
+
emit('dblclick:row', event, item.item);
|
|
1938
1839
|
},
|
|
1939
1840
|
item: base.item,
|
|
1940
1841
|
text: base.text,
|
|
@@ -2005,7 +1906,7 @@ watch(
|
|
|
2005
1906
|
/** This table's own data rows (excludes expand rows and nested tables). */
|
|
2006
1907
|
const dataRows = () =>
|
|
2007
1908
|
tableRef.value?.querySelectorAll<HTMLElement>(
|
|
2008
|
-
':scope >
|
|
1909
|
+
':scope > .bb-table__body > .bb-table-data__row'
|
|
2009
1910
|
);
|
|
2010
1911
|
|
|
2011
1912
|
const focusRow = (index: number) => {
|
|
@@ -2228,6 +2129,10 @@ watch(
|
|
|
2228
2129
|
if (tableRef.value && props.fixedColumns?.length) {
|
|
2229
2130
|
applyFixedTableColumns(tableRef.value, props.fixedColumns);
|
|
2230
2131
|
}
|
|
2132
|
+
// With a custom `#thead` the observed cells may live on a data row that
|
|
2133
|
+
// just arrived, or was re-keyed and replaced: re-arm the observer. The
|
|
2134
|
+
// default header row is stable, so this is skipped for it.
|
|
2135
|
+
if (slots.thead) nextTick(observeColumns);
|
|
2231
2136
|
},
|
|
2232
2137
|
{ flush: 'post' }
|
|
2233
2138
|
);
|
|
@@ -2283,6 +2188,14 @@ export type ItemKey<Item = any> = NestedPaths<Item> | (string & {});
|
|
|
2283
2188
|
*/
|
|
2284
2189
|
export type ReservedColumnKey = 'actions' | 'select';
|
|
2285
2190
|
|
|
2191
|
+
/**
|
|
2192
|
+
* Suffixes of the header affix slots (`header:prepend` / `header:append` and
|
|
2193
|
+
* their per-column `header:<key>:prepend/append` variants). A column key that
|
|
2194
|
+
* normalizes to one of these shadows the table-wide slot name; the table-wide
|
|
2195
|
+
* interpretation wins and dev builds warn.
|
|
2196
|
+
*/
|
|
2197
|
+
export type HeaderAffixSuffix = 'prepend' | 'append';
|
|
2198
|
+
|
|
2286
2199
|
/**
|
|
2287
2200
|
* Skeleton placeholder style for the loading state.
|
|
2288
2201
|
* Matches the expected content type for a better loading experience.
|
|
@@ -2353,19 +2266,19 @@ export type BbTableColumn<Item = any> = BaseColumn<Item> & {
|
|
|
2353
2266
|
sorted?: 'asc' | 'desc';
|
|
2354
2267
|
|
|
2355
2268
|
/**
|
|
2356
|
-
* Classes this column contributes to the whole row
|
|
2269
|
+
* Classes this column contributes to the whole row element. Every column's
|
|
2357
2270
|
* `rowClass` is accumulated with the table-level `rowClass` prop and applied
|
|
2358
2271
|
* together. Can also be a function of the cell's content for dynamic values.
|
|
2359
2272
|
*/
|
|
2360
2273
|
rowClass?: ColumnClasses<Item>;
|
|
2361
2274
|
|
|
2362
2275
|
/**
|
|
2363
|
-
* Defines the classes to be passed to
|
|
2276
|
+
* Defines the classes to be passed to this column's data cells. Can also be a function for dynamic values.
|
|
2364
2277
|
*/
|
|
2365
2278
|
tdClass?: ColumnClasses<Item>;
|
|
2366
2279
|
|
|
2367
2280
|
/**
|
|
2368
|
-
* Defines the classes to be passed to
|
|
2281
|
+
* Defines the classes to be passed to this column's header cell.
|
|
2369
2282
|
*/
|
|
2370
2283
|
thClass?: Classes;
|
|
2371
2284
|
|
|
@@ -2427,7 +2340,7 @@ export type BbTableProps<Item = any> = {
|
|
|
2427
2340
|
/**
|
|
2428
2341
|
* Opt a nested table into inheriting its column widths from an ancestor
|
|
2429
2342
|
* table's matching tracks (accounting for `select`/`actions` columns). This
|
|
2430
|
-
* also
|
|
2343
|
+
* also makes the parent's tracks authoritative, so it is **off by default** — a nested
|
|
2431
2344
|
* table renders independently unless you ask for alignment.
|
|
2432
2345
|
* - `false` (the default): do not inherit; size independently.
|
|
2433
2346
|
* - `true`: inherit from the nearest ancestor table.
|
|
@@ -2508,7 +2421,8 @@ export type BbTableProps<Item = any> = {
|
|
|
2508
2421
|
displayCaption?: boolean;
|
|
2509
2422
|
|
|
2510
2423
|
/**
|
|
2511
|
-
*
|
|
2424
|
+
* Splits the width equally among the columns that declare no `width`
|
|
2425
|
+
* (each undeclared column becomes a `minmax(0, 1fr)` track)
|
|
2512
2426
|
* making each column take up the same amount of space.
|
|
2513
2427
|
*/
|
|
2514
2428
|
fixed?: boolean;
|
|
@@ -2654,13 +2568,13 @@ export type BbTableProps<Item = any> = {
|
|
|
2654
2568
|
readonly?: boolean;
|
|
2655
2569
|
|
|
2656
2570
|
/**
|
|
2657
|
-
* Defines the classes to be passed to each data
|
|
2571
|
+
* Defines the classes to be passed to each data row. Can also be a
|
|
2658
2572
|
* function of the row's item for dynamic values.
|
|
2659
2573
|
*/
|
|
2660
2574
|
rowClass?: RowClasses<Item>;
|
|
2661
2575
|
|
|
2662
2576
|
/**
|
|
2663
|
-
* Defines the classes to be passed to the header
|
|
2577
|
+
* Defines the classes to be passed to the header row.
|
|
2664
2578
|
*/
|
|
2665
2579
|
headerRowClass?: Classes;
|
|
2666
2580
|
|
|
@@ -2710,12 +2624,12 @@ export type BbTableProps<Item = any> = {
|
|
|
2710
2624
|
selectText?: string;
|
|
2711
2625
|
|
|
2712
2626
|
/**
|
|
2713
|
-
* Defines the classes to be passed to
|
|
2627
|
+
* Defines the classes to be passed to every data cell. Can also be a function for dynamic values.
|
|
2714
2628
|
*/
|
|
2715
2629
|
tdClass?: ColumnClasses<Item>;
|
|
2716
2630
|
|
|
2717
2631
|
/**
|
|
2718
|
-
* Defines the classes to be passed to
|
|
2632
|
+
* Defines the classes to be passed to every header cell.
|
|
2719
2633
|
*/
|
|
2720
2634
|
thClass?: Classes;
|
|
2721
2635
|
|
|
@@ -2791,7 +2705,7 @@ export type MappedItem = Option & {
|
|
|
2791
2705
|
* Projection that represents a header
|
|
2792
2706
|
*/
|
|
2793
2707
|
export type MappedHeader = {
|
|
2794
|
-
align:
|
|
2708
|
+
align: string;
|
|
2795
2709
|
classes?: Classes;
|
|
2796
2710
|
key: string;
|
|
2797
2711
|
label: string;
|
|
@@ -2821,9 +2735,12 @@ export type ExpandControllerProps = {
|
|
|
2821
2735
|
};
|
|
2822
2736
|
|
|
2823
2737
|
export type BbTableEvents = {
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2738
|
+
// Row events carry `(event, item)` only, where `item` is the RAW record
|
|
2739
|
+
// from `items` — not the table's internal row wrapper. Selection state is
|
|
2740
|
+
// derivable from the selection v-model, so it is not repeated here.
|
|
2741
|
+
(e: 'click:row', event: MouseEvent, item: any): void;
|
|
2742
|
+
(e: 'contextmenu:row', event: MouseEvent, item: any): void;
|
|
2743
|
+
(e: 'dblclick:row', event: MouseEvent, item: any): void;
|
|
2827
2744
|
(e: 'update:highlighted', value: any): void;
|
|
2828
2745
|
(e: 'update:modelValue', value: any): void;
|
|
2829
2746
|
(e: 'update:selectAll', value: boolean): void;
|
|
@@ -2834,22 +2751,58 @@ export type BbTableEvents = {
|
|
|
2834
2751
|
(e: 'item:unselected', value: any): void;
|
|
2835
2752
|
};
|
|
2836
2753
|
|
|
2754
|
+
/**
|
|
2755
|
+
* Props exposed by the table-wide `header:prepend` / `header:append` slots and
|
|
2756
|
+
* their per-column `header:<key>:prepend` / `header:<key>:append` variants —
|
|
2757
|
+
* the `header:<key>` payload plus the column key.
|
|
2758
|
+
*/
|
|
2759
|
+
export type BbTableHeaderAffixSlotProps<Item = any> = {
|
|
2760
|
+
/** Merged header classes for this column. */
|
|
2761
|
+
classes?: Classes;
|
|
2762
|
+
/**
|
|
2763
|
+
* The raw key of the column being rendered — branch on it in the
|
|
2764
|
+
* table-wide slots.
|
|
2765
|
+
*/
|
|
2766
|
+
columnKey: string;
|
|
2767
|
+
/** The current table items. */
|
|
2768
|
+
items?: Item[];
|
|
2769
|
+
/** The column's label. */
|
|
2770
|
+
label?: string;
|
|
2771
|
+
/** Whether this column is sortable. */
|
|
2772
|
+
sortable?: boolean;
|
|
2773
|
+
/** This column's current sort direction, or `null` when unsorted. */
|
|
2774
|
+
sortOrder?: 'asc' | 'desc' | null;
|
|
2775
|
+
/** Cycles this column's sort: unsorted → asc → desc → unsorted. */
|
|
2776
|
+
toggleSort?: () => void;
|
|
2777
|
+
};
|
|
2778
|
+
|
|
2837
2779
|
export type BbTableSlots<Item = any> = {
|
|
2838
2780
|
/**
|
|
2839
|
-
* Replaces the
|
|
2781
|
+
* Replaces the content of the header row group (`.bb-table__head`). Use when
|
|
2782
|
+
* you need full control over the header markup. The table is a CSS grid,
|
|
2783
|
+
* not a `<table>`: provide role-complete rows — `<div role="row">` holding
|
|
2784
|
+
* one `<div role="columnheader">` per column — and never a `<tr>`/`<th>`.
|
|
2785
|
+
* Rows are subgrid rows, so each cell lands on the matching column track.
|
|
2840
2786
|
*/
|
|
2841
2787
|
thead?: (props: object) => any;
|
|
2842
2788
|
/**
|
|
2843
|
-
* Replaces the
|
|
2789
|
+
* Replaces the content of the body row group (`.bb-table__body`). Use when
|
|
2790
|
+
* you need full control over the body markup. Provide role-complete rows —
|
|
2791
|
+
* `<div role="row">` holding one `<div role="cell">` per column — never a
|
|
2792
|
+
* `<tr>`/`<td>`. A full-width cell spans with `grid-column: 1 / -1`.
|
|
2844
2793
|
*/
|
|
2845
2794
|
tbody?: (props: object) => any;
|
|
2846
2795
|
/**
|
|
2847
|
-
*
|
|
2796
|
+
* Fills the footer row group (`.bb-table__foot`). Use when you need a
|
|
2797
|
+
* footer. Provide role-complete rows — `<div role="row">` holding
|
|
2798
|
+
* `<div role="cell">` elements — never a `<tr>`/`<td>`.
|
|
2848
2799
|
*/
|
|
2849
2800
|
tfoot?: (props: {
|
|
2850
2801
|
/**
|
|
2851
|
-
* Total rendered column count
|
|
2852
|
-
*
|
|
2802
|
+
* Total rendered column count (accounts for the selection/actions
|
|
2803
|
+
* columns), so you never hand-count it. A footer cell spanning the
|
|
2804
|
+
* whole table uses `grid-column: 1 / -1`; one spanning a subset uses
|
|
2805
|
+
* `grid-column: span N` with `N` up to this value.
|
|
2853
2806
|
*/
|
|
2854
2807
|
columnCount: number;
|
|
2855
2808
|
}) => any;
|
|
@@ -2869,6 +2822,20 @@ export type BbTableSlots<Item = any> = {
|
|
|
2869
2822
|
* @param text - The accessible label text for the actions column header.
|
|
2870
2823
|
*/
|
|
2871
2824
|
'header:actions'?: (props: { text: string }) => any;
|
|
2825
|
+
/**
|
|
2826
|
+
* Content rendered before every data column's header cell content — the
|
|
2827
|
+
* label + sort control, or the column's own `header:<key>` replacement.
|
|
2828
|
+
* A column's `header:<key>:prepend` slot wins over this one for that
|
|
2829
|
+
* column; they never stack. Branch on `columnKey` for per-column output.
|
|
2830
|
+
*/
|
|
2831
|
+
'header:prepend'?: (props: BbTableHeaderAffixSlotProps<Item>) => any;
|
|
2832
|
+
/**
|
|
2833
|
+
* Content rendered after every data column's header cell content — the
|
|
2834
|
+
* label + sort control, or the column's own `header:<key>` replacement.
|
|
2835
|
+
* A column's `header:<key>:append` slot wins over this one for that
|
|
2836
|
+
* column; they never stack. Branch on `columnKey` for per-column output.
|
|
2837
|
+
*/
|
|
2838
|
+
'header:append'?: (props: BbTableHeaderAffixSlotProps<Item>) => any;
|
|
2872
2839
|
/**
|
|
2873
2840
|
* Content shown while table rows are loading (replaces the skeleton rows).
|
|
2874
2841
|
* @param items - The current (possibly stale) list of items, useful to match the previous row count.
|
|
@@ -2876,9 +2843,9 @@ export type BbTableSlots<Item = any> = {
|
|
|
2876
2843
|
loading?: (props: { items: Item[] }) => any;
|
|
2877
2844
|
/**
|
|
2878
2845
|
* Content shown when the items list is empty and not loading. It fills the
|
|
2879
|
-
* table's own full-width empty-state cell (
|
|
2880
|
-
*
|
|
2881
|
-
* illustration, a "clear filters" button — never a
|
|
2846
|
+
* table's own full-width empty-state cell (it already spans the
|
|
2847
|
+
* selection/actions columns), so provide the *content* — a message, an
|
|
2848
|
+
* illustration, a "clear filters" button — never a row/cell of your own.
|
|
2882
2849
|
*/
|
|
2883
2850
|
'no-data'?: (props: object) => any;
|
|
2884
2851
|
/**
|
|
@@ -2950,6 +2917,11 @@ export type BbTableSlots<Item = any> = {
|
|
|
2950
2917
|
string,
|
|
2951
2918
|
(props: {
|
|
2952
2919
|
classes?: Classes[];
|
|
2920
|
+
/**
|
|
2921
|
+
* Present only in the `header:<key>:prepend/append` affix slots — the
|
|
2922
|
+
* raw key of the column being rendered.
|
|
2923
|
+
*/
|
|
2924
|
+
columnKey?: string;
|
|
2953
2925
|
content?: any;
|
|
2954
2926
|
expanded?: boolean;
|
|
2955
2927
|
expandProps?: ExpandControllerProps;
|
|
@@ -2977,6 +2949,12 @@ export type BbTableSlots<Item = any> = {
|
|
|
2977
2949
|
|
|
2978
2950
|
```css
|
|
2979
2951
|
.bb-table {
|
|
2952
|
+
/* The table's surface colour. Cells are transparent at rest so the row can
|
|
2953
|
+
tint them, but anything that sticks over scrolled content — the stuck
|
|
2954
|
+
head band and `fixed-columns` cells — must be opaque, and paints this.
|
|
2955
|
+
Set it on the table (`.my-table { --bg: … }`) when the table sits on a
|
|
2956
|
+
surface that is not `--bb-panel`; a nested table re-declares its own. */
|
|
2957
|
+
--bg: var(--bb-panel);
|
|
2980
2958
|
--padding-x: 12px;
|
|
2981
2959
|
--cell-h: 36px;
|
|
2982
2960
|
--padding-y: 6px;
|
|
@@ -2993,9 +2971,6 @@ export type BbTableSlots<Item = any> = {
|
|
|
2993
2971
|
--offset-internal-end: var(--offset-end);
|
|
2994
2972
|
--offset-external-start: 0px;
|
|
2995
2973
|
--offset-external-end: 0px;
|
|
2996
|
-
/* Slack the fill column adds to reach the parent's width. Reset per table so a
|
|
2997
|
-
nested table never inherits an ancestor's value through the cascade. */
|
|
2998
|
-
--fill: 0px;
|
|
2999
2974
|
display: grid;
|
|
3000
2975
|
/* Override the UA `min-inline-size: min-content` that a <fieldset> container
|
|
3001
2976
|
(rendered for selectable tables) carries — without this the container can't
|
|
@@ -3004,14 +2979,6 @@ export type BbTableSlots<Item = any> = {
|
|
|
3004
2979
|
min-width: 0;
|
|
3005
2980
|
overflow-x: auto;
|
|
3006
2981
|
|
|
3007
|
-
/* While a container resize waits on the debounced column re-probe, the stale
|
|
3008
|
-
(wider) column widths would flash a horizontal scrollbar; clip instead of
|
|
3009
|
-
scroll for that window. Applied only when the table wasn't already
|
|
3010
|
-
overflowing, so no live scroll position is ever clipped away. */
|
|
3011
|
-
&--measuring {
|
|
3012
|
-
overflow-x: clip;
|
|
3013
|
-
}
|
|
3014
|
-
|
|
3015
2982
|
padding-inline-end: var(--offset-external-end);
|
|
3016
2983
|
padding-inline-start: var(--offset-external-start);
|
|
3017
2984
|
|
|
@@ -3035,89 +3002,133 @@ export type BbTableSlots<Item = any> = {
|
|
|
3035
3002
|
--padding-y: 4px;
|
|
3036
3003
|
}
|
|
3037
3004
|
|
|
3005
|
+
/* The visible caption keeps the UA `<caption>` centring it used to get. */
|
|
3006
|
+
&-caption {
|
|
3007
|
+
text-align: center;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
/* --- The grid -------------------------------------------------------------
|
|
3011
|
+
The root owns the column tracks (`grid-template-columns` is set inline
|
|
3012
|
+
from the column definitions) and every rowgroup and row is a subgrid of
|
|
3013
|
+
it, so a cell lands on its track through two levels without any width
|
|
3014
|
+
of its own. Real boxes at every level, never `display: contents`: the
|
|
3015
|
+
rowgroups host the busy bar and the loading opacity, and only a real box
|
|
3016
|
+
can be sticky.
|
|
3017
|
+
|
|
3018
|
+
`gap: 0` on the root is the authority — the heir of the old
|
|
3019
|
+
`border-spacing: 0` preflight reset. A subgrid-level `gap: 0` CANNOT
|
|
3020
|
+
neutralise a gap set on the root: the subgrid closes its gutters by
|
|
3021
|
+
growing its edge cells into the half-gutters (200px tracks came out
|
|
3022
|
+
215/230/215), so the root value is the only real protection; the two
|
|
3023
|
+
subgrid-level declarations below are defence in depth only. */
|
|
3024
|
+
&__table {
|
|
3025
|
+
display: grid;
|
|
3026
|
+
gap: 0;
|
|
3027
|
+
/* Reproduces the old `<table>` box: fills the container when the tracks
|
|
3028
|
+
sum below it, grows to the tracks' sum (and scrolls the container) when
|
|
3029
|
+
they don't. Bare `min-width: 100%` would leave this box at container
|
|
3030
|
+
width with the tracks overflowing it — row backgrounds and the busy bar
|
|
3031
|
+
would paint short and `%` widths would resolve against the wrong box.
|
|
3032
|
+
Nothing paints on the root itself: rowgroups and rows are the paint
|
|
3033
|
+
surfaces. */
|
|
3034
|
+
min-width: 100%;
|
|
3035
|
+
width: fit-content;
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
&__head,
|
|
3039
|
+
&__body,
|
|
3040
|
+
&__foot {
|
|
3041
|
+
display: grid;
|
|
3042
|
+
gap: 0;
|
|
3043
|
+
grid-column: 1 / -1;
|
|
3044
|
+
grid-template-columns: subgrid;
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
/* Anchors the busy bar (`::after` below). Positioned unconditionally so the
|
|
3048
|
+
fixed-header rule (a plain `sticky`, higher specificity) never has to fight
|
|
3049
|
+
a loading-only `relative`. */
|
|
3050
|
+
&__head {
|
|
3051
|
+
position: relative;
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
&__head > [role='row'],
|
|
3055
|
+
&__body > [role='row'],
|
|
3056
|
+
&__foot > [role='row'] {
|
|
3057
|
+
display: grid;
|
|
3058
|
+
gap: 0;
|
|
3059
|
+
grid-column: 1 / -1;
|
|
3060
|
+
grid-template-columns: subgrid;
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
/* Sticky headers live on the head ROWGROUP: a sticky cell or row inside the
|
|
3064
|
+
double subgrid cannot stick in any engine (an item's vertical sticky slack
|
|
3065
|
+
is bounded by its own grid container — the one-row-tall row). Descendant
|
|
3066
|
+
form on purpose, so a nested table's head sticks along, as it always did.
|
|
3067
|
+
The stuck band paints `--bg` so scrolled rows never show through it.
|
|
3068
|
+
Sticky-column header cells (inline sticky from `fixed-columns`) sit above
|
|
3069
|
+
their scrolled siblings within the band. */
|
|
3038
3070
|
&--fixed-header {
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
position: sticky;
|
|
3048
|
-
top: 0;
|
|
3049
|
-
z-index: 2;
|
|
3050
|
-
}
|
|
3051
|
-
}
|
|
3071
|
+
.bb-table__head {
|
|
3072
|
+
background-color: var(--bg);
|
|
3073
|
+
position: sticky;
|
|
3074
|
+
top: 0;
|
|
3075
|
+
z-index: 1;
|
|
3076
|
+
}
|
|
3077
|
+
.bb-table__head [style*='sticky'] {
|
|
3078
|
+
z-index: 2;
|
|
3052
3079
|
}
|
|
3053
3080
|
}
|
|
3054
3081
|
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3082
|
+
/* A `fixed-columns` cell (inline `position: sticky` from the component)
|
|
3083
|
+
slides over its scrolled siblings, so it cannot stay transparent. It
|
|
3084
|
+
paints the row's current tint when the row has one (`--row-bg`, set by
|
|
3085
|
+
the hover / highlighted states below), else the table surface — so a
|
|
3086
|
+
pinned cell never reads as a hole in a hovered or current row. */
|
|
3087
|
+
[style*='sticky'] {
|
|
3088
|
+
background-color: var(--row-bg, var(--bg));
|
|
3089
|
+
/* Same curve and duration as the row's own tint transition, so a pinned
|
|
3090
|
+
cell fades in step with its row instead of snapping ahead of it. */
|
|
3091
|
+
transition: background-color var(--bb-transition-fast) var(--bb-ease);
|
|
3062
3092
|
}
|
|
3063
3093
|
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
cell would drift by 2px per gridline and break nested-table
|
|
3069
|
-
alignment. */
|
|
3070
|
-
border-collapse: separate;
|
|
3071
|
-
border-spacing: 0;
|
|
3072
|
-
min-width: 100%;
|
|
3094
|
+
&--selectable {
|
|
3095
|
+
[role='row'] > .bb-table-data__cell--select {
|
|
3096
|
+
text-align: center;
|
|
3097
|
+
}
|
|
3073
3098
|
}
|
|
3074
3099
|
|
|
3075
|
-
/* A nested table inheriting its parent's widths sizes to
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
it overrides their width/min-width at equal specificity. */
|
|
3100
|
+
/* A nested table inheriting its parent's widths sizes to its tracks (the
|
|
3101
|
+
parent's, republished as custom properties) instead of filling its
|
|
3102
|
+
container, so its columns stay aligned with the parent's. */
|
|
3079
3103
|
&--inherit-widths {
|
|
3080
3104
|
/* Never grow past the parent's expand cell: a child with more columns
|
|
3081
|
-
than the parent overflows the cell, so cap the container here
|
|
3082
|
-
|
|
3083
|
-
rather than stretching the ancestor. */
|
|
3105
|
+
than the parent overflows the cell, so cap the container here rather
|
|
3106
|
+
than stretching the ancestor. */
|
|
3084
3107
|
max-width: 100%;
|
|
3085
3108
|
min-width: 0;
|
|
3086
3109
|
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
&--fixed {
|
|
3104
|
-
> table {
|
|
3105
|
-
table-layout: fixed;
|
|
3106
|
-
width: 100%;
|
|
3107
|
-
|
|
3108
|
-
thead {
|
|
3109
|
-
.bb-table-header--actions {
|
|
3110
|
-
width: auto;
|
|
3111
|
-
}
|
|
3112
|
-
}
|
|
3113
|
-
}
|
|
3114
|
-
|
|
3115
|
-
table {
|
|
3116
|
-
min-width: var(--natural-width, 0px);
|
|
3117
|
-
}
|
|
3110
|
+
/* No scrollport of its own — the ancestor's is the one that matters.
|
|
3111
|
+
A child that inherits its parent's tracks is sized to the parent's
|
|
3112
|
+
CONTENT width, so a scrollport here would have its right edge off in
|
|
3113
|
+
the overflow where nobody can see it, and `position: sticky` from
|
|
3114
|
+
`fixed-columns` would affix to that instead of to the visible edge:
|
|
3115
|
+
the parent's actions pin correctly while the child's sit hundreds of
|
|
3116
|
+
pixels off-screen until you scroll all the way right.
|
|
3117
|
+
|
|
3118
|
+
Scrolling it separately was never coherent anyway. The whole point of
|
|
3119
|
+
`inherit-column-widths` is that the child's columns line up with the
|
|
3120
|
+
parent's, and an independent scrollport breaks that alignment the
|
|
3121
|
+
moment either one moves. One scrollport, one scroll position, columns
|
|
3122
|
+
that stay aligned — and sticky that affixes where the reader is
|
|
3123
|
+
looking. Both axes: with only `overflow-x` set, `overflow-y` computes
|
|
3124
|
+
to `auto` and the scrollport comes back. */
|
|
3125
|
+
overflow: visible;
|
|
3118
3126
|
}
|
|
3119
3127
|
|
|
3120
3128
|
&-header {
|
|
3129
|
+
/* Centres the content vertically like the UA did for table cells; the
|
|
3130
|
+
cell stays a block container so header affixes keep their inline flow. */
|
|
3131
|
+
align-content: center;
|
|
3121
3132
|
border-bottom: var(--bb-border-w) solid var(--bb-border);
|
|
3122
3133
|
color: var(--bb-text-muted);
|
|
3123
3134
|
font-size: var(--bb-fs);
|
|
@@ -3131,16 +3142,13 @@ export type BbTableSlots<Item = any> = {
|
|
|
3131
3142
|
&--right {
|
|
3132
3143
|
text-align: right;
|
|
3133
3144
|
}
|
|
3134
|
-
|
|
3135
|
-
&--actions {
|
|
3136
|
-
width: 0;
|
|
3137
|
-
}
|
|
3138
3145
|
}
|
|
3139
3146
|
|
|
3140
|
-
/* Sortable header control: label +
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3147
|
+
/* Sortable header control: label + one arrow. Invisible at rest; hovering
|
|
3148
|
+
or focusing the header previews a faint arrow (the direction the next
|
|
3149
|
+
click sorts by); an active sort shows it solid — up for ascending,
|
|
3150
|
+
rotated down for descending (AG Grid convention) — and the third click
|
|
3151
|
+
clears it. The opacity ladder and the rotation both transition. */
|
|
3144
3152
|
&-sort {
|
|
3145
3153
|
align-items: center;
|
|
3146
3154
|
background: none;
|
|
@@ -3165,60 +3173,87 @@ export type BbTableSlots<Item = any> = {
|
|
|
3165
3173
|
}
|
|
3166
3174
|
|
|
3167
3175
|
.bb-table-sort__icon {
|
|
3168
|
-
|
|
3176
|
+
--arrow-preview: 0.45;
|
|
3177
|
+
color: var(--bb-text);
|
|
3169
3178
|
flex-shrink: 0;
|
|
3170
3179
|
opacity: 0;
|
|
3171
|
-
transition:
|
|
3180
|
+
transition:
|
|
3181
|
+
opacity var(--bb-transition-fast) var(--bb-ease),
|
|
3182
|
+
transform var(--bb-transition-fast) var(--bb-ease);
|
|
3172
3183
|
}
|
|
3173
3184
|
|
|
3174
|
-
/*
|
|
3175
|
-
|
|
3176
|
-
.
|
|
3177
|
-
|
|
3178
|
-
transition: color var(--bb-transition-fast) var(--bb-ease);
|
|
3185
|
+
/* Preview: faint arrow while the header is hovered or focused. */
|
|
3186
|
+
&:hover .bb-table-sort__icon,
|
|
3187
|
+
&:focus-visible .bb-table-sort__icon {
|
|
3188
|
+
opacity: var(--arrow-preview);
|
|
3179
3189
|
}
|
|
3180
3190
|
|
|
3181
|
-
|
|
3182
|
-
&:focus-visible .bb-table-sort__icon,
|
|
3191
|
+
/* Active sort: solid, and the second direction flips the arrow. */
|
|
3183
3192
|
&--asc .bb-table-sort__icon,
|
|
3184
|
-
&--desc .bb-table-sort__icon
|
|
3193
|
+
&--desc .bb-table-sort__icon,
|
|
3194
|
+
&--asc:hover .bb-table-sort__icon,
|
|
3195
|
+
&--desc:hover .bb-table-sort__icon {
|
|
3185
3196
|
opacity: 1;
|
|
3186
3197
|
}
|
|
3187
3198
|
|
|
3188
|
-
&--
|
|
3189
|
-
|
|
3190
|
-
color: var(--bb-text);
|
|
3199
|
+
&--desc .bb-table-sort__icon {
|
|
3200
|
+
transform: rotate(180deg);
|
|
3191
3201
|
}
|
|
3192
3202
|
}
|
|
3193
3203
|
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
padding: var(--padding-y) var(--padding-x);
|
|
3207
|
-
}
|
|
3204
|
+
/* Every cell of this table's own body rows (data, structural, skeleton,
|
|
3205
|
+
empty-state, expand, loading) — the `:scope >`-style child chain keeps a
|
|
3206
|
+
nested table's cells out. The row step is wrapped in `:where()` so these
|
|
3207
|
+
rules stay at (0,2,0): the `<table>` era's `.bb-table tbody tr td` was
|
|
3208
|
+
(0,1,3), and consumer overrides scoped to one class + a cell class that
|
|
3209
|
+
won then must keep winning now. */
|
|
3210
|
+
&__body > :where([role='row']) {
|
|
3211
|
+
/* Row separators on the cells, not the row, so a full-width cell and a
|
|
3212
|
+
data cell draw the same line. */
|
|
3213
|
+
&:where(:not(:last-child)) > [role='cell'] {
|
|
3214
|
+
border-bottom: var(--bb-border-w) solid var(--bb-border);
|
|
3215
|
+
}
|
|
3208
3216
|
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3217
|
+
> [role='cell'] {
|
|
3218
|
+
/* Centres like the UA `vertical-align: middle` of table cells. */
|
|
3219
|
+
align-content: center;
|
|
3220
|
+
color: var(--bb-text);
|
|
3221
|
+
font-size: var(--bb-fs);
|
|
3222
|
+
font-weight: 400;
|
|
3223
|
+
/* A minimum, as a `<td>` height always was: a grid box's `height` is a
|
|
3224
|
+
hard cap and would clip wrapped content. */
|
|
3225
|
+
min-height: var(--cell-h);
|
|
3226
|
+
padding: var(--padding-y) var(--padding-x);
|
|
3227
|
+
}
|
|
3214
3228
|
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3229
|
+
> .bb-table-expand__cell {
|
|
3230
|
+
/* Collapse to the content's height instead of inheriting the data-row
|
|
3231
|
+
cell height, so an expand row with no slot content takes no space. */
|
|
3232
|
+
min-height: 0;
|
|
3233
|
+
padding: 0;
|
|
3234
|
+
|
|
3235
|
+
&:empty {
|
|
3236
|
+
display: none;
|
|
3218
3237
|
}
|
|
3219
3238
|
}
|
|
3220
3239
|
}
|
|
3221
3240
|
|
|
3241
|
+
/* Full-width cells span every track of their subgrid row. `contain:
|
|
3242
|
+
inline-size` is what keeps them from sizing the tracks: without it a
|
|
3243
|
+
spanning item's max-content contribution back-pressures the columns
|
|
3244
|
+
(300px tracks became 500px), and with a nested inheriting table inside
|
|
3245
|
+
that is a runaway loop. Containment holds the parent's tracks exactly;
|
|
3246
|
+
the content still overflows visibly, the ancestor scrollport still
|
|
3247
|
+
scrolls it, and sticky descendants keep working. Scoped to these three
|
|
3248
|
+
cells only — on every cell it would zero every intrinsic contribution. */
|
|
3249
|
+
&-expand__cell,
|
|
3250
|
+
&-no-data__cell,
|
|
3251
|
+
&-loading__cell {
|
|
3252
|
+
contain: inline-size;
|
|
3253
|
+
grid-column: 1 / -1;
|
|
3254
|
+
min-width: 0;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3222
3257
|
/* Apply the structural edge offsets to the first/last *data* column. They
|
|
3223
3258
|
must land on the data cells, not the absolute first/last child: a select
|
|
3224
3259
|
or actions column would otherwise absorb the fill and leave the data
|
|
@@ -3228,15 +3263,15 @@ export type BbTableSlots<Item = any> = {
|
|
|
3228
3263
|
reserved as extra edge padding, but a *negative* offset (a select/actions
|
|
3229
3264
|
column reclaimed from this column's width) must not eat into the normal
|
|
3230
3265
|
`--padding-x` — that only shrinks the column width, not its inner padding. */
|
|
3231
|
-
|
|
3232
|
-
|
|
3266
|
+
&__head > [role='row'] > .bb-table-header--edge-start,
|
|
3267
|
+
&__body > [role='row'] > .bb-table-data__cell--edge-start {
|
|
3233
3268
|
padding-inline-start: calc(
|
|
3234
3269
|
var(--padding-x) + max(0px, var(--offset-internal-start))
|
|
3235
3270
|
);
|
|
3236
3271
|
}
|
|
3237
3272
|
|
|
3238
|
-
|
|
3239
|
-
|
|
3273
|
+
&__head > [role='row'] > .bb-table-header--edge-end,
|
|
3274
|
+
&__body > [role='row'] > .bb-table-data__cell--edge-end {
|
|
3240
3275
|
padding-inline-end: calc(
|
|
3241
3276
|
var(--padding-x) + max(0px, var(--offset-internal-end))
|
|
3242
3277
|
);
|
|
@@ -3245,8 +3280,8 @@ export type BbTableSlots<Item = any> = {
|
|
|
3245
3280
|
&.bb-table--loading {
|
|
3246
3281
|
.bb-table-loading__row {
|
|
3247
3282
|
.bb-table-loading__cell {
|
|
3283
|
+
align-content: start;
|
|
3248
3284
|
padding: 0 var(--padding-x) !important;
|
|
3249
|
-
vertical-align: top;
|
|
3250
3285
|
}
|
|
3251
3286
|
}
|
|
3252
3287
|
|
|
@@ -3254,6 +3289,7 @@ export type BbTableSlots<Item = any> = {
|
|
|
3254
3289
|
.bb-table-loading__cell {
|
|
3255
3290
|
border: none;
|
|
3256
3291
|
height: 0;
|
|
3292
|
+
min-height: 0;
|
|
3257
3293
|
overflow: hidden;
|
|
3258
3294
|
padding: 0 !important;
|
|
3259
3295
|
}
|
|
@@ -3262,15 +3298,17 @@ export type BbTableSlots<Item = any> = {
|
|
|
3262
3298
|
|
|
3263
3299
|
&-skeleton {
|
|
3264
3300
|
&__cell {
|
|
3265
|
-
|
|
3266
|
-
|
|
3301
|
+
align-content: center;
|
|
3302
|
+
min-height: var(--cell-h);
|
|
3303
|
+
/* `clip`, not `hidden`: a scroll container zeroes a grid item's
|
|
3304
|
+
automatic minimum, which let skeleton tracks compress below the
|
|
3305
|
+
shimmer's width and cut it flush at the column border. */
|
|
3306
|
+
overflow: clip;
|
|
3267
3307
|
padding: var(--padding-y) var(--padding-x);
|
|
3268
|
-
vertical-align: middle;
|
|
3269
3308
|
|
|
3270
3309
|
&--select,
|
|
3271
3310
|
&--actions {
|
|
3272
3311
|
text-align: center;
|
|
3273
|
-
width: 0;
|
|
3274
3312
|
}
|
|
3275
3313
|
}
|
|
3276
3314
|
|
|
@@ -3375,9 +3413,9 @@ export type BbTableSlots<Item = any> = {
|
|
|
3375
3413
|
centres itself regardless of the table's column alignment. */
|
|
3376
3414
|
&-no-data {
|
|
3377
3415
|
&__cell {
|
|
3416
|
+
align-content: center;
|
|
3378
3417
|
padding: var(--padding-y) var(--padding-x);
|
|
3379
3418
|
text-align: center;
|
|
3380
|
-
vertical-align: middle;
|
|
3381
3419
|
}
|
|
3382
3420
|
|
|
3383
3421
|
&__text {
|
|
@@ -3387,15 +3425,20 @@ export type BbTableSlots<Item = any> = {
|
|
|
3387
3425
|
|
|
3388
3426
|
&-data {
|
|
3389
3427
|
&__row {
|
|
3428
|
+
/* Keep a row on one printed page. */
|
|
3429
|
+
break-inside: avoid;
|
|
3390
3430
|
transition: background-color var(--bb-transition-fast) var(--bb-ease);
|
|
3391
3431
|
|
|
3432
|
+
/* Row states publish their fill as `--row-bg` too, so pinned
|
|
3433
|
+
(sticky) cells — which must be opaque — paint the same colour. */
|
|
3392
3434
|
&:hover {
|
|
3393
|
-
|
|
3435
|
+
--row-bg: var(--bb-surface-hover);
|
|
3436
|
+
background-color: var(--row-bg);
|
|
3394
3437
|
}
|
|
3395
3438
|
|
|
3396
3439
|
/* Focusable only with `keyboardNavigation`; the ring is mandatory
|
|
3397
|
-
focus indication, not decoration. Inset outline
|
|
3398
|
-
|
|
3440
|
+
focus indication, not decoration. Inset outline so it never
|
|
3441
|
+
escapes the scrollport. */
|
|
3399
3442
|
&:focus-visible {
|
|
3400
3443
|
outline: var(--bb-ring-size) solid var(--bb-ring);
|
|
3401
3444
|
outline-offset: calc(var(--bb-ring-size) * -1);
|
|
@@ -3413,19 +3456,18 @@ export type BbTableSlots<Item = any> = {
|
|
|
3413
3456
|
while the pointer is over it. Consumers can still override the whole
|
|
3414
3457
|
look via `row-class`. */
|
|
3415
3458
|
&--highlighted {
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3459
|
+
--row-bg: color-mix(in oklab, var(--bb-primary) 8%, var(--bg));
|
|
3460
|
+
background-color: var(--row-bg);
|
|
3461
|
+
|
|
3462
|
+
/* The accent bar sits on the first CELL, not the row: a pinned
|
|
3463
|
+
(opaque) first column would otherwise cover it. Same position
|
|
3464
|
+
either way — the first cell is the row's left edge. */
|
|
3465
|
+
> :first-child {
|
|
3466
|
+
box-shadow: inset 3px 0 0 0 var(--bb-primary);
|
|
3467
|
+
}
|
|
3422
3468
|
|
|
3423
3469
|
&:hover {
|
|
3424
|
-
|
|
3425
|
-
in oklab,
|
|
3426
|
-
var(--bb-primary) 15%,
|
|
3427
|
-
var(--bb-panel)
|
|
3428
|
-
);
|
|
3470
|
+
--row-bg: color-mix(in oklab, var(--bb-primary) 15%, var(--bg));
|
|
3429
3471
|
}
|
|
3430
3472
|
}
|
|
3431
3473
|
}
|
|
@@ -3443,6 +3485,14 @@ export type BbTableSlots<Item = any> = {
|
|
|
3443
3485
|
|
|
3444
3486
|
&__cell {
|
|
3445
3487
|
&--actions {
|
|
3488
|
+
/* Controls sit at the row's end. Invisible on a root table (the
|
|
3489
|
+
track IS the content width) — it matters in a nested table, whose
|
|
3490
|
+
actions track is widened to the parent's region so the two cells
|
|
3491
|
+
are equal and the controls line up under the parent's. */
|
|
3492
|
+
text-align: end;
|
|
3493
|
+
/* Track-sizing-critical: the actions track is `max-content`, and
|
|
3494
|
+
`nowrap` is what makes that the width of the controls laid side by
|
|
3495
|
+
side rather than a wrapped stack. */
|
|
3446
3496
|
white-space: nowrap;
|
|
3447
3497
|
> *:not(:first-child) {
|
|
3448
3498
|
margin-left: var(--actions-spacing);
|
|
@@ -3481,23 +3531,23 @@ export type BbTableSlots<Item = any> = {
|
|
|
3481
3531
|
dependency change (a filter, a poll, an autosave round-trip). Keep the rows
|
|
3482
3532
|
on screen, mark them stale, and run an indeterminate bar along the header/body
|
|
3483
3533
|
boundary. `aria-busy` on the root already carries the semantics and the
|
|
3484
|
-
sr-only row in
|
|
3534
|
+
sr-only row in the body still announces, so the bar is decorative — hence a
|
|
3485
3535
|
pseudo-element rather than a node. `.bb-table--empty` is "nothing to keep". */
|
|
3486
3536
|
.bb-table--loading:not(.bb-table--empty) {
|
|
3487
|
-
&
|
|
3537
|
+
& .bb-table__body {
|
|
3488
3538
|
opacity: 0.55;
|
|
3489
3539
|
transition: opacity var(--bb-transition-fast) ease;
|
|
3490
3540
|
}
|
|
3491
3541
|
|
|
3492
|
-
/*
|
|
3493
|
-
the bar rides along with
|
|
3494
|
-
& thead {
|
|
3495
|
-
position: relative;
|
|
3496
|
-
}
|
|
3542
|
+
/* Anchored to the head rowgroup (positioned above). With `fixed-header`
|
|
3543
|
+
the head is sticky, so the bar rides along with the stuck band.
|
|
3497
3544
|
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3545
|
+
MUST stay absolutely positioned: on a subgrid box a non-positioned
|
|
3546
|
+
pseudo-element becomes a grid item and inserts a phantom row (the header
|
|
3547
|
+
row doubled in height). Animating background-position repaints a 2px
|
|
3548
|
+
strip instead of laying out a moving node every frame, and needs no
|
|
3549
|
+
wrapper to clip it. */
|
|
3550
|
+
& .bb-table__head::after {
|
|
3501
3551
|
animation: bb-table-busy-slide 1.1s linear infinite;
|
|
3502
3552
|
background: linear-gradient(
|
|
3503
3553
|
90deg,
|
|
@@ -3528,7 +3578,7 @@ export type BbTableSlots<Item = any> = {
|
|
|
3528
3578
|
}
|
|
3529
3579
|
|
|
3530
3580
|
@media (prefers-reduced-motion: reduce) {
|
|
3531
|
-
.bb-table--loading:not(.bb-table--empty)
|
|
3581
|
+
.bb-table--loading:not(.bb-table--empty) .bb-table__head::after {
|
|
3532
3582
|
animation: none;
|
|
3533
3583
|
background: var(--bb-primary);
|
|
3534
3584
|
opacity: 0.5;
|