@unofficialbox/box-open-elements 0.5.0 → 0.7.0
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 +27 -73
- package/dist/components/collections/table.d.ts +113 -3
- package/dist/components/collections/table.js +540 -37
- package/dist/components/feedback/due-badge.d.ts +38 -0
- package/dist/components/feedback/due-badge.js +168 -0
- package/dist/components/feedback/due-types.d.ts +32 -0
- package/dist/components/feedback/due-types.js +91 -0
- package/dist/components/feedback/progress-steps.d.ts +51 -5
- package/dist/components/feedback/progress-steps.js +200 -22
- package/dist/components/feedback/stage-path.d.ts +44 -0
- package/dist/components/feedback/stage-path.js +235 -0
- package/dist/components/forms/select.d.ts +21 -0
- package/dist/components/forms/select.js +100 -3
- package/dist/components/forms/text-field.d.ts +21 -0
- package/dist/components/forms/text-field.js +100 -1
- package/dist/components/layout/nav-sidebar.d.ts +14 -0
- package/dist/components/layout/nav-sidebar.js +49 -0
- package/dist/components/layout/split-view.d.ts +14 -0
- package/dist/components/layout/split-view.js +80 -1
- package/dist/components/overlays/command-palette.d.ts +62 -0
- package/dist/components/overlays/command-palette.js +537 -0
- package/dist/components/overlays/command-types.d.ts +90 -0
- package/dist/components/overlays/command-types.js +259 -0
- package/dist/components/overlays/drawer.d.ts +18 -0
- package/dist/components/overlays/drawer.js +135 -9
- package/dist/components/overlays/shortcuts-overlay.d.ts +48 -0
- package/dist/components/overlays/shortcuts-overlay.js +385 -0
- package/dist/components/overlays/tooltip.js +1 -1
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +2 -0
- package/dist/core/offset-window.d.ts +70 -0
- package/dist/core/offset-window.js +134 -0
- package/dist/core/virtualize.d.ts +71 -0
- package/dist/core/virtualize.js +67 -0
- package/dist/element-maps.d.ts +292 -0
- package/dist/element-maps.js +1 -0
- package/dist/entries/activity-density.d.ts +1 -0
- package/dist/entries/activity-density.js +1 -0
- package/dist/entries/agent-chat.d.ts +1 -0
- package/dist/entries/agent-chat.js +1 -0
- package/dist/entries/audit-log.d.ts +1 -0
- package/dist/entries/audit-log.js +1 -0
- package/dist/entries/command-palette.d.ts +1 -0
- package/dist/entries/command-palette.js +1 -0
- package/dist/entries/compare-view.d.ts +1 -0
- package/dist/entries/compare-view.js +1 -0
- package/dist/entries/diff-viewer.d.ts +1 -0
- package/dist/entries/diff-viewer.js +1 -0
- package/dist/entries/due-badge.d.ts +1 -0
- package/dist/entries/due-badge.js +1 -0
- package/dist/entries/form-wizard.d.ts +1 -0
- package/dist/entries/form-wizard.js +1 -0
- package/dist/entries/lineage-graph.d.ts +1 -0
- package/dist/entries/lineage-graph.js +1 -0
- package/dist/entries/notification-bell.d.ts +1 -0
- package/dist/entries/notification-bell.js +1 -0
- package/dist/entries/notification-inbox.d.ts +1 -0
- package/dist/entries/notification-inbox.js +1 -0
- package/dist/entries/provenance-strip.d.ts +1 -0
- package/dist/entries/provenance-strip.js +1 -0
- package/dist/entries/run-trace.d.ts +1 -0
- package/dist/entries/run-trace.js +1 -0
- package/dist/entries/shortcuts-overlay.d.ts +1 -0
- package/dist/entries/shortcuts-overlay.js +1 -0
- package/dist/entries/signature-ceremony.d.ts +1 -0
- package/dist/entries/signature-ceremony.js +1 -0
- package/dist/entries/stage-path.d.ts +1 -0
- package/dist/entries/stage-path.js +1 -0
- package/dist/entries/timeline.d.ts +1 -0
- package/dist/entries/timeline.js +1 -0
- package/dist/entries/version-graph.d.ts +1 -0
- package/dist/entries/version-graph.js +1 -0
- package/dist/entries/version-list.d.ts +1 -0
- package/dist/entries/version-list.js +1 -0
- package/dist/entries/wizard-summary.d.ts +1 -0
- package/dist/entries/wizard-summary.js +1 -0
- package/dist/entries/work-queue.d.ts +1 -0
- package/dist/entries/work-queue.js +1 -0
- package/dist/entries/workload-board.d.ts +1 -0
- package/dist/entries/workload-board.js +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +21 -0
- package/dist/patterns/agent-chat/agent-chat.d.ts +75 -0
- package/dist/patterns/agent-chat/agent-chat.js +820 -0
- package/dist/patterns/agent-chat/controller.d.ts +27 -0
- package/dist/patterns/agent-chat/controller.js +182 -0
- package/dist/patterns/agent-chat/index.d.ts +3 -0
- package/dist/patterns/agent-chat/index.js +6 -0
- package/dist/patterns/agent-chat/types.d.ts +109 -0
- package/dist/patterns/agent-chat/types.js +1 -0
- package/dist/patterns/audit/activity-density.d.ts +68 -0
- package/dist/patterns/audit/activity-density.js +428 -0
- package/dist/patterns/audit/audit-log.d.ts +182 -0
- package/dist/patterns/audit/audit-log.js +1082 -0
- package/dist/patterns/audit/index.d.ts +3 -0
- package/dist/patterns/audit/index.js +7 -0
- package/dist/patterns/audit/shared.d.ts +10 -0
- package/dist/patterns/audit/shared.js +50 -0
- package/dist/patterns/audit/types.d.ts +143 -0
- package/dist/patterns/audit/types.js +327 -0
- package/dist/patterns/audit/window.d.ts +116 -0
- package/dist/patterns/audit/window.js +132 -0
- package/dist/patterns/content-explorer/actions/controller.js +12 -2
- package/dist/patterns/content-explorer/box-transport.js +117 -25
- package/dist/patterns/content-explorer/content-explorer.d.ts +13 -1
- package/dist/patterns/content-explorer/content-explorer.js +36 -3
- package/dist/patterns/content-explorer/contracts.d.ts +24 -1
- package/dist/patterns/content-explorer/contracts.js +68 -11
- package/dist/patterns/content-explorer/controller.d.ts +16 -1
- package/dist/patterns/content-explorer/controller.js +130 -4
- package/dist/patterns/content-explorer/navigation/controller.js +11 -9
- package/dist/patterns/content-explorer/types.d.ts +58 -1
- package/dist/patterns/content-picker/content-picker.d.ts +71 -0
- package/dist/patterns/content-picker/content-picker.js +747 -0
- package/dist/patterns/content-picker/controller.d.ts +38 -0
- package/dist/patterns/content-picker/controller.js +141 -0
- package/dist/patterns/content-picker/index.d.ts +3 -0
- package/dist/patterns/content-picker/index.js +6 -0
- package/dist/patterns/content-picker/types.d.ts +52 -0
- package/dist/patterns/content-picker/types.js +26 -0
- package/dist/patterns/content-sidebar/content-sidebar.d.ts +43 -0
- package/dist/patterns/content-sidebar/content-sidebar.js +293 -0
- package/dist/patterns/content-sidebar/index.d.ts +2 -0
- package/dist/patterns/content-sidebar/index.js +4 -0
- package/dist/patterns/content-sidebar/types.d.ts +18 -0
- package/dist/patterns/content-sidebar/types.js +20 -0
- package/dist/patterns/content-uploader/box-transport.d.ts +14 -0
- package/dist/patterns/content-uploader/box-transport.js +54 -0
- package/dist/patterns/content-uploader/content-uploader.d.ts +62 -0
- package/dist/patterns/content-uploader/content-uploader.js +559 -0
- package/dist/patterns/content-uploader/controller.d.ts +39 -0
- package/dist/patterns/content-uploader/controller.js +227 -0
- package/dist/patterns/content-uploader/index.d.ts +4 -0
- package/dist/patterns/content-uploader/index.js +8 -0
- package/dist/patterns/content-uploader/types.d.ts +120 -0
- package/dist/patterns/content-uploader/types.js +38 -0
- package/dist/patterns/diff/compare-view.d.ts +47 -0
- package/dist/patterns/diff/compare-view.js +276 -0
- package/dist/patterns/diff/diff-viewer.d.ts +42 -0
- package/dist/patterns/diff/diff-viewer.js +434 -0
- package/dist/patterns/diff/engine.d.ts +22 -0
- package/dist/patterns/diff/engine.js +276 -0
- package/dist/patterns/diff/index.d.ts +4 -0
- package/dist/patterns/diff/index.js +8 -0
- package/dist/patterns/diff/types.d.ts +69 -0
- package/dist/patterns/diff/types.js +24 -0
- package/dist/patterns/form-wizard/controller.d.ts +39 -0
- package/dist/patterns/form-wizard/controller.js +157 -0
- package/dist/patterns/form-wizard/form-wizard.d.ts +63 -0
- package/dist/patterns/form-wizard/form-wizard.js +468 -0
- package/dist/patterns/form-wizard/index.d.ts +4 -0
- package/dist/patterns/form-wizard/index.js +8 -0
- package/dist/patterns/form-wizard/types.d.ts +109 -0
- package/dist/patterns/form-wizard/types.js +83 -0
- package/dist/patterns/form-wizard/wizard-summary.d.ts +62 -0
- package/dist/patterns/form-wizard/wizard-summary.js +324 -0
- package/dist/patterns/internal/safe-href.d.ts +17 -0
- package/dist/patterns/internal/safe-href.js +17 -0
- package/dist/patterns/lineage/index.d.ts +3 -0
- package/dist/patterns/lineage/index.js +6 -0
- package/dist/patterns/lineage/lineage-graph.d.ts +30 -0
- package/dist/patterns/lineage/lineage-graph.js +434 -0
- package/dist/patterns/lineage/provenance-strip.d.ts +22 -0
- package/dist/patterns/lineage/provenance-strip.js +194 -0
- package/dist/patterns/lineage/types.d.ts +53 -0
- package/dist/patterns/lineage/types.js +49 -0
- package/dist/patterns/notifications/index.d.ts +3 -0
- package/dist/patterns/notifications/index.js +6 -0
- package/dist/patterns/notifications/notification-bell.d.ts +38 -0
- package/dist/patterns/notifications/notification-bell.js +198 -0
- package/dist/patterns/notifications/notification-inbox.d.ts +46 -0
- package/dist/patterns/notifications/notification-inbox.js +537 -0
- package/dist/patterns/notifications/types.d.ts +61 -0
- package/dist/patterns/notifications/types.js +108 -0
- package/dist/patterns/preview/annotation-inspector.d.ts +25 -6
- package/dist/patterns/preview/annotation-inspector.js +239 -94
- package/dist/patterns/preview/annotation-thread.d.ts +25 -5
- package/dist/patterns/preview/annotation-thread.js +233 -82
- package/dist/patterns/preview/annotation-toolbar.d.ts +20 -6
- package/dist/patterns/preview/annotation-toolbar.js +162 -96
- package/dist/patterns/preview/content-preview-adapter.d.ts +10 -1
- package/dist/patterns/preview/content-preview-adapter.js +26 -1
- package/dist/patterns/preview/preview-element.d.ts +15 -2
- package/dist/patterns/preview/preview-element.js +223 -52
- package/dist/patterns/preview/provider-adapter.d.ts +47 -0
- package/dist/patterns/preview/provider-adapter.js +17 -0
- package/dist/patterns/run/index.d.ts +2 -0
- package/dist/patterns/run/index.js +4 -0
- package/dist/patterns/run/run-trace.d.ts +40 -0
- package/dist/patterns/run/run-trace.js +441 -0
- package/dist/patterns/run/types.d.ts +66 -0
- package/dist/patterns/run/types.js +152 -0
- package/dist/patterns/signature/index.d.ts +2 -0
- package/dist/patterns/signature/index.js +4 -0
- package/dist/patterns/signature/signature-ceremony.d.ts +33 -0
- package/dist/patterns/signature/signature-ceremony.js +317 -0
- package/dist/patterns/signature/types.d.ts +68 -0
- package/dist/patterns/signature/types.js +89 -0
- package/dist/patterns/timeline/index.d.ts +2 -0
- package/dist/patterns/timeline/index.js +4 -0
- package/dist/patterns/timeline/timeline.d.ts +27 -0
- package/dist/patterns/timeline/timeline.js +430 -0
- package/dist/patterns/timeline/types.d.ts +36 -0
- package/dist/patterns/timeline/types.js +27 -0
- package/dist/patterns/versions/graph-layout.d.ts +43 -0
- package/dist/patterns/versions/graph-layout.js +159 -0
- package/dist/patterns/versions/index.d.ts +4 -0
- package/dist/patterns/versions/index.js +8 -0
- package/dist/patterns/versions/shared.d.ts +4 -0
- package/dist/patterns/versions/shared.js +13 -0
- package/dist/patterns/versions/types.d.ts +37 -0
- package/dist/patterns/versions/types.js +39 -0
- package/dist/patterns/versions/version-graph.d.ts +36 -0
- package/dist/patterns/versions/version-graph.js +438 -0
- package/dist/patterns/versions/version-list.d.ts +39 -0
- package/dist/patterns/versions/version-list.js +407 -0
- package/dist/patterns/work-queue/controller.d.ts +26 -0
- package/dist/patterns/work-queue/controller.js +150 -0
- package/dist/patterns/work-queue/index.d.ts +4 -0
- package/dist/patterns/work-queue/index.js +7 -0
- package/dist/patterns/work-queue/types.d.ts +128 -0
- package/dist/patterns/work-queue/types.js +45 -0
- package/dist/patterns/work-queue/work-queue.d.ts +48 -0
- package/dist/patterns/work-queue/work-queue.js +534 -0
- package/dist/patterns/work-queue/workload-board.d.ts +53 -0
- package/dist/patterns/work-queue/workload-board.js +608 -0
- package/package.json +89 -5
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import { BaseElement } from "../../core/index.js";
|
|
1
|
+
import { BaseElement, resolveRowWindow, sameRowWindow } from "../../core/index.js";
|
|
2
2
|
import { boePanel, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
3
|
+
import { isSafeHref } from "../../patterns/internal/safe-href.js";
|
|
3
4
|
const DEFAULT_TAG_NAME = "box-table";
|
|
5
|
+
const CELL_TONES = new Set(["neutral", "brand", "success", "warning", "error"]);
|
|
6
|
+
const CELL_ALIGNS = new Set(["start", "end", "center"]);
|
|
7
|
+
/** `align` is author input from a JSON attribute like everything else — an
|
|
8
|
+
* unvalidated value would close the attribute and inject markup. */
|
|
9
|
+
const alignAttr = (align) => align && CELL_ALIGNS.has(align) ? ` data-align="${align}"` : "";
|
|
10
|
+
const isCellDescriptor = (value) => typeof value === "object" && value !== null && typeof value.text === "string";
|
|
11
|
+
/** Escape a value for use inside a double-quoted attribute selector. */
|
|
12
|
+
const cssAttrValue = (value) => value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
4
13
|
const escapeHtml = (value) => value
|
|
5
14
|
.replaceAll("&", "&")
|
|
6
15
|
.replaceAll("<", "<")
|
|
@@ -47,8 +56,35 @@ const tableStyles = `
|
|
|
47
56
|
th[part="sortable"] {
|
|
48
57
|
cursor: pointer;
|
|
49
58
|
user-select: none;
|
|
59
|
+
/* The button carries the padding so its hit area fills the cell. */
|
|
60
|
+
padding: 0;
|
|
50
61
|
}
|
|
51
62
|
th[part="sortable"]:hover { color: var(--boe-token-text-text, #222222); }
|
|
63
|
+
|
|
64
|
+
/* A real button, so sorting is reachable by keyboard: focus, Enter and
|
|
65
|
+
Space come from the platform instead of a keydown reimplementation. */
|
|
66
|
+
[part="sort-button"] {
|
|
67
|
+
appearance: none;
|
|
68
|
+
display: block;
|
|
69
|
+
width: 100%;
|
|
70
|
+
padding: ${boeSpace[2]} ${boeSpace[3]};
|
|
71
|
+
border: 0;
|
|
72
|
+
background: transparent;
|
|
73
|
+
font: inherit;
|
|
74
|
+
letter-spacing: inherit;
|
|
75
|
+
text-transform: inherit;
|
|
76
|
+
text-align: inherit;
|
|
77
|
+
color: inherit;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
th[data-align="end"] [part="sort-button"] { text-align: end; }
|
|
82
|
+
th[data-align="center"] [part="sort-button"] { text-align: center; }
|
|
83
|
+
|
|
84
|
+
[part="sort-button"]:focus-visible {
|
|
85
|
+
outline: none;
|
|
86
|
+
box-shadow: inset 0 0 0 2px var(--boe-token-surface-surface-brand, #0061d5);
|
|
87
|
+
}
|
|
52
88
|
th[aria-sort] .boe-sort-arrow::after { content: " ↕"; opacity: 0.5; }
|
|
53
89
|
th[aria-sort="ascending"] .boe-sort-arrow::after { content: " ↑"; opacity: 1; }
|
|
54
90
|
th[aria-sort="descending"] .boe-sort-arrow::after { content: " ↓"; opacity: 1; }
|
|
@@ -76,7 +112,156 @@ const tableStyles = `
|
|
|
76
112
|
tbody tr[part="row"]:focus-visible td {
|
|
77
113
|
box-shadow: inset 0 0 0 2px var(--boe-token-surface-surface-brand, #0061d5);
|
|
78
114
|
}
|
|
115
|
+
|
|
116
|
+
/* Windowing needs a bounded viewport to scroll within: --boe-table-height is
|
|
117
|
+
the host's knob, defaulting to a sensible page-sized band. Sticky headers
|
|
118
|
+
already work inside this scroller. */
|
|
119
|
+
:host([virtualize]) [part="shell"] {
|
|
120
|
+
max-block-size: var(--boe-table-height, 26rem);
|
|
121
|
+
overflow-y: auto;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
tr[part="spacer"] td {
|
|
125
|
+
border: 0;
|
|
126
|
+
padding: 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
[part="cell-badge"] {
|
|
130
|
+
display: inline-block;
|
|
131
|
+
padding: 0.08rem 0.5rem;
|
|
132
|
+
border-radius: 999px;
|
|
133
|
+
font-size: 0.74rem;
|
|
134
|
+
font-weight: 700;
|
|
135
|
+
color: color-mix(in srgb, var(--cell-tone, var(--boe-token-text-text-secondary, #6f6f6f)) 76%, black 24%);
|
|
136
|
+
background: color-mix(in srgb, var(--cell-tone, var(--boe-token-text-text-secondary, #6f6f6f)) 14%, transparent);
|
|
137
|
+
border: 1px solid color-mix(in srgb, var(--cell-tone, var(--boe-token-text-text-secondary, #6f6f6f)) 30%, transparent);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
[part="cell-link"] {
|
|
141
|
+
color: var(--boe-token-surface-surface-brand, #0061d5);
|
|
142
|
+
text-decoration: none;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
[part="cell-link"]:hover {
|
|
146
|
+
text-decoration: underline;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
[part="expander"] {
|
|
150
|
+
appearance: none;
|
|
151
|
+
display: inline-grid;
|
|
152
|
+
place-items: center;
|
|
153
|
+
inline-size: 1.4rem;
|
|
154
|
+
block-size: 1.4rem;
|
|
155
|
+
border: 0;
|
|
156
|
+
border-radius: 4px;
|
|
157
|
+
background: transparent;
|
|
158
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
159
|
+
font: inherit;
|
|
160
|
+
font-size: 0.72rem;
|
|
161
|
+
cursor: pointer;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
[part="expander"]:hover {
|
|
165
|
+
background: var(--boe-token-surface-surface-hover, #f4f4f4);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
[part="expander"]:focus-visible {
|
|
169
|
+
outline: 2px solid color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 34%, transparent);
|
|
170
|
+
outline-offset: 1px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
[part="expander"]::before {
|
|
174
|
+
content: "▸";
|
|
175
|
+
transition: transform 120ms ease;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
[part="expander"][aria-expanded="true"]::before {
|
|
179
|
+
transform: rotate(90deg);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
tr[part="detail-row"] td {
|
|
183
|
+
background: color-mix(in srgb, var(--boe-token-surface-surface-secondary, #fbfbfb) 80%, transparent);
|
|
184
|
+
font-size: 0.84rem;
|
|
185
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
tr[part="state-row"] td {
|
|
189
|
+
padding: 1.1rem ${boeSpace[3]};
|
|
190
|
+
text-align: center;
|
|
191
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
tr[part="state-row"][data-state="error"] td {
|
|
195
|
+
color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 76%, black 24%);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
[part="state-spinner"] {
|
|
199
|
+
display: inline-block;
|
|
200
|
+
vertical-align: -0.15rem;
|
|
201
|
+
margin-inline-end: 0.45rem;
|
|
202
|
+
inline-size: 0.95rem;
|
|
203
|
+
block-size: 0.95rem;
|
|
204
|
+
border-radius: 999px;
|
|
205
|
+
border: 2px solid color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 20%, transparent);
|
|
206
|
+
border-top-color: var(--boe-token-surface-surface-brand, #0061d5);
|
|
207
|
+
animation: boe-table-spin 0.9s linear infinite;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@keyframes boe-table-spin {
|
|
211
|
+
to { transform: rotate(360deg); }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@media (prefers-reduced-motion: reduce) {
|
|
215
|
+
[part="state-spinner"] { animation-duration: 1.6s; }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* Stacked rows: each row becomes a card, each cell states its column via
|
|
219
|
+
data-label. Roles are explicit on rows and cells, so the grid semantics
|
|
220
|
+
survive the display change. stacked="always" forces it; stacked="auto"
|
|
221
|
+
stacks when the host container narrows. */
|
|
222
|
+
:host {
|
|
223
|
+
container-type: inline-size;
|
|
224
|
+
}
|
|
225
|
+
` +
|
|
226
|
+
(() => {
|
|
227
|
+
const rules = (scope) => `
|
|
228
|
+
${scope} thead { display: none; }
|
|
229
|
+
${scope} tr[part="row"] {
|
|
230
|
+
display: block;
|
|
231
|
+
padding: ${boeSpace[2]} 0;
|
|
232
|
+
}
|
|
233
|
+
${scope} tbody td {
|
|
234
|
+
display: grid;
|
|
235
|
+
grid-template-columns: minmax(7.5rem, 9rem) 1fr;
|
|
236
|
+
gap: 0.5rem;
|
|
237
|
+
border-bottom: none;
|
|
238
|
+
padding-block: 0.18rem;
|
|
239
|
+
text-align: start;
|
|
240
|
+
}
|
|
241
|
+
${scope} tbody td[data-label]::before {
|
|
242
|
+
content: attr(data-label);
|
|
243
|
+
font-size: 0.74rem;
|
|
244
|
+
font-weight: 700;
|
|
245
|
+
letter-spacing: 0.05em;
|
|
246
|
+
text-transform: uppercase;
|
|
247
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
248
|
+
align-self: center;
|
|
249
|
+
}
|
|
250
|
+
${scope} tr[part="row"]:not(:last-child) {
|
|
251
|
+
border-bottom: 1px solid var(--boe-token-stroke-stroke, #e8e8e8);
|
|
252
|
+
}
|
|
253
|
+
${scope} tr[part="detail-row"] td,
|
|
254
|
+
${scope} tr[part="state-row"] td {
|
|
255
|
+
display: block;
|
|
256
|
+
}
|
|
79
257
|
`;
|
|
258
|
+
return `
|
|
259
|
+
${rules(':host([stacked]:not([stacked="auto"]))')}
|
|
260
|
+
@container (max-width: 560px) {
|
|
261
|
+
${rules(':host([stacked="auto"])')}
|
|
262
|
+
}
|
|
263
|
+
`;
|
|
264
|
+
})();
|
|
80
265
|
/**
|
|
81
266
|
* A semantic, selectable data table — box-ui-elements `Table` + `makeSelectable`.
|
|
82
267
|
* Rows and columns come from JSON. Selection supports single / Ctrl-click toggle
|
|
@@ -87,12 +272,135 @@ const tableStyles = `
|
|
|
87
272
|
export class Table extends BaseElement {
|
|
88
273
|
static tagName = DEFAULT_TAG_NAME;
|
|
89
274
|
static get observedAttributes() {
|
|
90
|
-
return [
|
|
275
|
+
return [
|
|
276
|
+
"columns",
|
|
277
|
+
"empty-text",
|
|
278
|
+
"error-text",
|
|
279
|
+
"label",
|
|
280
|
+
"loading",
|
|
281
|
+
"rows",
|
|
282
|
+
"selection-mode",
|
|
283
|
+
"sort-direction",
|
|
284
|
+
"sort-key",
|
|
285
|
+
"stacked",
|
|
286
|
+
"virtualize",
|
|
287
|
+
"row-height",
|
|
288
|
+
];
|
|
91
289
|
}
|
|
92
290
|
bodyEl;
|
|
93
291
|
headEl;
|
|
292
|
+
shellEl;
|
|
94
293
|
selected = new Set();
|
|
95
294
|
anchorIndex = -1;
|
|
295
|
+
expandedIds = new Set();
|
|
296
|
+
lastWindow = null;
|
|
297
|
+
/**
|
|
298
|
+
* Whether the last render actually windowed. Not the same as `virtualize`:
|
|
299
|
+
* the flag is suppressed for a collection the fixed-height window cannot
|
|
300
|
+
* describe (see `update`). Cached at render time so the scroll handler does
|
|
301
|
+
* not re-parse `rows` on every frame to answer the question.
|
|
302
|
+
*/
|
|
303
|
+
windowing = false;
|
|
304
|
+
scrollFrame = 0;
|
|
305
|
+
measuredRowHeight = 0;
|
|
306
|
+
measureFrame = 0;
|
|
307
|
+
/** True while rows are being fetched: the body states "Loading rows…". */
|
|
308
|
+
get loading() {
|
|
309
|
+
return this.hasAttribute("loading");
|
|
310
|
+
}
|
|
311
|
+
set loading(value) {
|
|
312
|
+
this.toggleAttribute("loading", Boolean(value));
|
|
313
|
+
}
|
|
314
|
+
/** What the body says when there are no rows and nothing is loading. */
|
|
315
|
+
get emptyText() {
|
|
316
|
+
return this.getAttribute("empty-text") ?? "No rows";
|
|
317
|
+
}
|
|
318
|
+
set emptyText(value) {
|
|
319
|
+
this.setAttribute("empty-text", value);
|
|
320
|
+
}
|
|
321
|
+
/** Non-empty puts the table in an error state stating this text. */
|
|
322
|
+
get errorText() {
|
|
323
|
+
return this.getAttribute("error-text") ?? "";
|
|
324
|
+
}
|
|
325
|
+
set errorText(value) {
|
|
326
|
+
if (value) {
|
|
327
|
+
this.setAttribute("error-text", value);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
this.removeAttribute("error-text");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Render only the rows near the viewport. Opt-in: a windowed table needs a
|
|
335
|
+
* bounded height to scroll within, and silently windowing an unbounded table
|
|
336
|
+
* would render nothing.
|
|
337
|
+
*
|
|
338
|
+
* A request, not a guarantee. The window arithmetic is fixed-height, so it is
|
|
339
|
+
* suppressed for a collection whose rows can change height — today that means
|
|
340
|
+
* any collection where a row declares `detail`. Read `renderedWindow` to see
|
|
341
|
+
* whether a render actually windowed.
|
|
342
|
+
*/
|
|
343
|
+
get virtualize() {
|
|
344
|
+
return this.hasAttribute("virtualize");
|
|
345
|
+
}
|
|
346
|
+
set virtualize(value) {
|
|
347
|
+
this.toggleAttribute("virtualize", Boolean(value));
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Row height in pixels for the window arithmetic — a starting estimate, not
|
|
351
|
+
* a contract. The real height depends on density, font, and zoom, and a
|
|
352
|
+
* declared value that is off by a fraction of a pixel makes the scroll range
|
|
353
|
+
* drift as you scroll (a 10k-row table measured ~55px of wobble at 37px
|
|
354
|
+
* declared). After the first paint the element measures a rendered row and
|
|
355
|
+
* uses that instead, so the estimate only has to be close enough to pick a
|
|
356
|
+
* sane first window.
|
|
357
|
+
*/
|
|
358
|
+
get rowHeight() {
|
|
359
|
+
if (this.measuredRowHeight > 0) {
|
|
360
|
+
return this.measuredRowHeight;
|
|
361
|
+
}
|
|
362
|
+
return this.declaredRowHeight;
|
|
363
|
+
}
|
|
364
|
+
get declaredRowHeight() {
|
|
365
|
+
const raw = Number(this.getAttribute("row-height"));
|
|
366
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 37;
|
|
367
|
+
}
|
|
368
|
+
set rowHeight(value) {
|
|
369
|
+
// A new declaration supersedes the old measurement; the next paint
|
|
370
|
+
// measures again.
|
|
371
|
+
this.measuredRowHeight = 0;
|
|
372
|
+
this.setAttribute("row-height", String(value));
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Adopt the real rendered row height, once, when it differs from the value
|
|
376
|
+
* the last window was computed with. Re-rendering with the measured height
|
|
377
|
+
* settles immediately: the next measurement matches, so this cannot loop.
|
|
378
|
+
*/
|
|
379
|
+
measureRowHeight() {
|
|
380
|
+
if (!this.windowing || this.measureFrame)
|
|
381
|
+
return;
|
|
382
|
+
const row = this.bodyEl.querySelector('[part="row"]');
|
|
383
|
+
const height = row?.getBoundingClientRect().height ?? 0;
|
|
384
|
+
if (height <= 0 || Math.abs(height - this.rowHeight) < 0.5)
|
|
385
|
+
return;
|
|
386
|
+
this.measuredRowHeight = height;
|
|
387
|
+
this.measureFrame = requestAnimationFrame(() => {
|
|
388
|
+
this.measureFrame = 0;
|
|
389
|
+
this.update();
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* The window the last render used, or null when it rendered every row — for
|
|
394
|
+
* tests and hosts. Null despite `virtualize` means the collection is one the
|
|
395
|
+
* fixed-height window cannot describe and windowing was suppressed.
|
|
396
|
+
*/
|
|
397
|
+
get renderedWindow() {
|
|
398
|
+
return this.windowing ? this.lastWindow : null;
|
|
399
|
+
}
|
|
400
|
+
/** Ids of the rows whose detail is expanded (host-readable). */
|
|
401
|
+
get expandedRows() {
|
|
402
|
+
return [...this.expandedIds];
|
|
403
|
+
}
|
|
96
404
|
get columns() {
|
|
97
405
|
return this.parseJson("columns", []);
|
|
98
406
|
}
|
|
@@ -139,12 +447,35 @@ export class Table extends BaseElement {
|
|
|
139
447
|
return fallback;
|
|
140
448
|
}
|
|
141
449
|
}
|
|
142
|
-
|
|
450
|
+
cellValue(row, column, index) {
|
|
143
451
|
if (Array.isArray(row.cells)) {
|
|
144
452
|
return row.cells[index] ?? "";
|
|
145
453
|
}
|
|
146
454
|
return row.cells[column.key] ?? "";
|
|
147
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* Descriptor rendering. Everything is escaped; a link with a non-http(s)
|
|
458
|
+
* href renders as plain text; a badge's tone colours a word that already
|
|
459
|
+
* carries the meaning.
|
|
460
|
+
*/
|
|
461
|
+
cellMarkup(value) {
|
|
462
|
+
if (!isCellDescriptor(value)) {
|
|
463
|
+
return escapeHtml(typeof value === "string" ? value : "");
|
|
464
|
+
}
|
|
465
|
+
if (value.kind === "badge") {
|
|
466
|
+
const tone = value.tone && CELL_TONES.has(value.tone) ? value.tone : "neutral";
|
|
467
|
+
const toneVar = tone === "neutral"
|
|
468
|
+
? "var(--boe-token-text-text-secondary, #6f6f6f)"
|
|
469
|
+
: tone === "brand"
|
|
470
|
+
? "var(--boe-token-surface-surface-brand, #0061d5)"
|
|
471
|
+
: `var(--boe-token-surface-status-surface-${tone}, #6f6f6f)`;
|
|
472
|
+
return `<span part="cell-badge" style="--cell-tone:${toneVar};">${escapeHtml(value.text)}</span>`;
|
|
473
|
+
}
|
|
474
|
+
if (value.kind === "link" && value.href && isSafeHref(value.href)) {
|
|
475
|
+
return `<a part="cell-link" href="${escapeHtml(value.href)}">${escapeHtml(value.text)}</a>`;
|
|
476
|
+
}
|
|
477
|
+
return escapeHtml(value.text);
|
|
478
|
+
}
|
|
148
479
|
renderTemplate() {
|
|
149
480
|
if (!this.shadowRoot)
|
|
150
481
|
return;
|
|
@@ -159,8 +490,23 @@ export class Table extends BaseElement {
|
|
|
159
490
|
`;
|
|
160
491
|
this.headEl = this.shadowRoot.querySelector('[part="header-row"]');
|
|
161
492
|
this.bodyEl = this.shadowRoot.querySelector('[part="body"]');
|
|
493
|
+
this.shellEl = this.shadowRoot.querySelector('[part="shell"]');
|
|
162
494
|
}
|
|
163
495
|
setupListeners() {
|
|
496
|
+
// Scroll fires continuously; coalesce to one re-render per frame, and skip
|
|
497
|
+
// entirely while the resolved slice is unchanged — most scroll events move
|
|
498
|
+
// the viewport within the rows already rendered.
|
|
499
|
+
this.shellEl.addEventListener("scroll", () => {
|
|
500
|
+
if (!this.windowing || this.scrollFrame)
|
|
501
|
+
return;
|
|
502
|
+
this.scrollFrame = requestAnimationFrame(() => {
|
|
503
|
+
this.scrollFrame = 0;
|
|
504
|
+
const next = this.currentWindow();
|
|
505
|
+
if (this.lastWindow && sameRowWindow(this.lastWindow, next))
|
|
506
|
+
return;
|
|
507
|
+
this.update();
|
|
508
|
+
});
|
|
509
|
+
}, { passive: true });
|
|
164
510
|
this.headEl.addEventListener("click", event => {
|
|
165
511
|
const th = event.target.closest('th[part="sortable"]');
|
|
166
512
|
if (!th)
|
|
@@ -168,6 +514,11 @@ export class Table extends BaseElement {
|
|
|
168
514
|
this.toggleSort(th.dataset.key ?? "");
|
|
169
515
|
});
|
|
170
516
|
this.bodyEl.addEventListener("click", event => {
|
|
517
|
+
const expander = event.target.closest('[part="expander"]');
|
|
518
|
+
if (expander) {
|
|
519
|
+
this.toggleRowDetail(expander.dataset.rowId ?? "");
|
|
520
|
+
return; // expanding is not selecting
|
|
521
|
+
}
|
|
171
522
|
const row = event.target.closest('[part="row"]');
|
|
172
523
|
if (!row)
|
|
173
524
|
return;
|
|
@@ -179,9 +530,34 @@ export class Table extends BaseElement {
|
|
|
179
530
|
rowElements() {
|
|
180
531
|
return Array.from(this.bodyEl.querySelectorAll('[part="row"]'));
|
|
181
532
|
}
|
|
533
|
+
toggleRowDetail(rowId) {
|
|
534
|
+
if (!rowId)
|
|
535
|
+
return;
|
|
536
|
+
const expanded = !this.expandedIds.has(rowId);
|
|
537
|
+
if (expanded) {
|
|
538
|
+
this.expandedIds.add(rowId);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
this.expandedIds.delete(rowId);
|
|
542
|
+
}
|
|
543
|
+
this.dispatchEvent(new CustomEvent("row-toggled", {
|
|
544
|
+
bubbles: true,
|
|
545
|
+
composed: true,
|
|
546
|
+
detail: { rowId, expanded },
|
|
547
|
+
}));
|
|
548
|
+
this.update();
|
|
549
|
+
// update() rewrote the body, destroying the button that was activated;
|
|
550
|
+
// put focus on its replacement so a keyboard user is not dropped.
|
|
551
|
+
this.bodyEl
|
|
552
|
+
.querySelector(`[part="expander"][data-row-id="${cssAttrValue(rowId)}"]`)
|
|
553
|
+
?.focus();
|
|
554
|
+
}
|
|
182
555
|
onBodyKeydown(event) {
|
|
183
|
-
|
|
184
|
-
|
|
556
|
+
// Keys aimed at the expander button (Enter/Space activate it natively)
|
|
557
|
+
// must not double as row selection.
|
|
558
|
+
if (event.target.closest('[part="expander"]'))
|
|
559
|
+
return;
|
|
560
|
+
if (this.rowElements().length === 0)
|
|
185
561
|
return;
|
|
186
562
|
const currentRow = event.target.closest('[part="row"]');
|
|
187
563
|
const index = currentRow ? Number(currentRow.dataset.index) : -1;
|
|
@@ -204,29 +580,55 @@ export class Table extends BaseElement {
|
|
|
204
580
|
this.activateRow(index, { toggle: this.selectionMode === "multiple" });
|
|
205
581
|
return;
|
|
206
582
|
}
|
|
583
|
+
// Bounds come from the data, not the DOM: when windowing, the rendered
|
|
584
|
+
// rows are a slice and `rows.length` would stop End at the bottom of the
|
|
585
|
+
// window instead of the bottom of the collection.
|
|
586
|
+
const lastIndex = this.rows.length - 1;
|
|
207
587
|
let nextIndex = index;
|
|
208
588
|
if (event.key === "ArrowDown")
|
|
209
|
-
nextIndex = Math.min(index + 1,
|
|
589
|
+
nextIndex = Math.min(index + 1, lastIndex);
|
|
210
590
|
else if (event.key === "ArrowUp")
|
|
211
591
|
nextIndex = Math.max(index - 1, 0);
|
|
212
592
|
else if (event.key === "Home")
|
|
213
593
|
nextIndex = 0;
|
|
214
594
|
else if (event.key === "End")
|
|
215
|
-
nextIndex =
|
|
595
|
+
nextIndex = lastIndex;
|
|
216
596
|
else
|
|
217
597
|
return;
|
|
218
598
|
event.preventDefault();
|
|
219
|
-
|
|
599
|
+
this.focusRowByIndex(nextIndex);
|
|
220
600
|
// Shift+Arrow extends the selection from the anchor.
|
|
221
601
|
if (event.shiftKey && this.selectionMode === "multiple") {
|
|
222
602
|
this.selectRange(this.anchorIndex < 0 ? index : this.anchorIndex, nextIndex);
|
|
223
603
|
}
|
|
224
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* Focus a row by its absolute index, bringing it into the window first when
|
|
607
|
+
* virtualizing — keyboard navigation must be able to reach a row that is not
|
|
608
|
+
* currently rendered, or Home/End stop at the edge of the window.
|
|
609
|
+
*/
|
|
610
|
+
focusRowByIndex(index) {
|
|
611
|
+
const find = () => this.bodyEl.querySelector(`[part="row"][data-index="${String(index)}"]`);
|
|
612
|
+
let target = find();
|
|
613
|
+
if (!target && this.windowing && this.shellEl) {
|
|
614
|
+
// Scroll the row to the top of the viewport, then re-render the window
|
|
615
|
+
// synchronously: the scroll event's own re-render lands a frame later,
|
|
616
|
+
// and focus cannot wait for it.
|
|
617
|
+
this.shellEl.scrollTop = index * this.rowHeight;
|
|
618
|
+
this.update();
|
|
619
|
+
target = find();
|
|
620
|
+
}
|
|
621
|
+
target?.focus();
|
|
622
|
+
}
|
|
225
623
|
toggleSort(key) {
|
|
226
624
|
const currentKey = this.getAttribute("sort-key");
|
|
227
625
|
const currentDir = this.getAttribute("sort-direction");
|
|
228
626
|
const direction = currentKey === key && currentDir === "ascending" ? "descending" : "ascending";
|
|
229
|
-
this.dispatchEvent(new CustomEvent("sort", {
|
|
627
|
+
this.dispatchEvent(new CustomEvent("sort", {
|
|
628
|
+
bubbles: true,
|
|
629
|
+
composed: true,
|
|
630
|
+
detail: { key, direction },
|
|
631
|
+
}));
|
|
230
632
|
}
|
|
231
633
|
activateRow(index, options = {}) {
|
|
232
634
|
const rows = this.rows;
|
|
@@ -276,6 +678,14 @@ export class Table extends BaseElement {
|
|
|
276
678
|
row.setAttribute("aria-selected", String(this.selected.has(id)));
|
|
277
679
|
}
|
|
278
680
|
}
|
|
681
|
+
currentWindow() {
|
|
682
|
+
return resolveRowWindow({
|
|
683
|
+
totalRows: this.rows.length,
|
|
684
|
+
rowHeight: this.rowHeight,
|
|
685
|
+
viewportHeight: this.shellEl?.clientHeight ?? 0,
|
|
686
|
+
scrollTop: this.shellEl?.scrollTop ?? 0,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
279
689
|
update() {
|
|
280
690
|
if (!this.bodyEl || !this.headEl)
|
|
281
691
|
return;
|
|
@@ -284,34 +694,127 @@ export class Table extends BaseElement {
|
|
|
284
694
|
const selectable = this.selectionMode !== "none";
|
|
285
695
|
const sortKey = this.getAttribute("sort-key");
|
|
286
696
|
const sortDir = this.getAttribute("sort-direction");
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
this.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
697
|
+
// A non-string detail is malformed input, not a crash: it gets no expander.
|
|
698
|
+
const expandable = rows.some(row => typeof row.detail === "string");
|
|
699
|
+
const columnCount = columns.length + (expandable ? 1 : 0);
|
|
700
|
+
const tableEl = this.shadowRoot?.querySelector('[part="table"]');
|
|
701
|
+
tableEl?.setAttribute("aria-label", this.label);
|
|
702
|
+
tableEl?.setAttribute("role", selectable ? "grid" : "table");
|
|
703
|
+
if (this.loading) {
|
|
704
|
+
tableEl?.setAttribute("aria-busy", "true");
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
tableEl?.removeAttribute("aria-busy");
|
|
708
|
+
}
|
|
709
|
+
this.headEl.innerHTML =
|
|
710
|
+
(expandable ? `<th scope="col" aria-label="Row details"></th>` : "") +
|
|
711
|
+
columns
|
|
712
|
+
.map(column => {
|
|
713
|
+
const align = alignAttr(column.align);
|
|
714
|
+
if (column.sortable) {
|
|
715
|
+
const sorted = sortKey === column.key;
|
|
716
|
+
const sort = sorted
|
|
717
|
+
? ` aria-sort="${sortDir === "descending" ? "descending" : "ascending"}"`
|
|
718
|
+
: ' aria-sort="none"';
|
|
719
|
+
// The state, in words as well as the arrow glyph.
|
|
720
|
+
const stateLabel = sorted
|
|
721
|
+
? `, sorted ${sortDir === "descending" ? "descending" : "ascending"}`
|
|
722
|
+
: ", not sorted";
|
|
723
|
+
return `<th part="sortable" scope="col" data-key="${escapeHtml(column.key)}"${align}${sort}><button type="button" part="sort-button" aria-label="Sort by ${escapeHtml(column.label)}${stateLabel}">${escapeHtml(column.label)}<span class="boe-sort-arrow" aria-hidden="true"></span></button></th>`;
|
|
724
|
+
}
|
|
725
|
+
return `<th scope="col"${align}>${escapeHtml(column.label)}</th>`;
|
|
726
|
+
})
|
|
727
|
+
.join("");
|
|
728
|
+
// A state row is never windowed. Cleared up front so a table that drops
|
|
729
|
+
// into loading/error/empty does not leave the previous window's flag
|
|
730
|
+
// standing — the scroll handler and `renderedWindow` both read it.
|
|
731
|
+
this.windowing = false;
|
|
732
|
+
this.lastWindow = null;
|
|
733
|
+
// Loading, error, and empty are stated in words in the body — a blank
|
|
734
|
+
// grid reads as broken. Loading wins; a stale error must not outlive a
|
|
735
|
+
// retry that is visibly in flight.
|
|
736
|
+
if (this.loading) {
|
|
737
|
+
this.bodyEl.innerHTML = `<tr part="state-row" data-state="loading" role="row"><td role="${selectable ? "gridcell" : "cell"}" colspan="${String(columnCount)}"><span part="state-spinner" aria-hidden="true"></span>Loading rows…</td></tr>`;
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (this.errorText) {
|
|
741
|
+
this.bodyEl.innerHTML = `<tr part="state-row" data-state="error" role="row"><td role="${selectable ? "gridcell" : "cell"}" colspan="${String(columnCount)}">${escapeHtml(this.errorText)}</td></tr>`;
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (rows.length === 0) {
|
|
745
|
+
this.bodyEl.innerHTML = `<tr part="state-row" data-state="empty" role="row"><td role="${selectable ? "gridcell" : "cell"}" colspan="${String(columnCount)}">${escapeHtml(this.emptyText)}</td></tr>`;
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
// Windowing renders only the rows near the viewport, padded with spacer
|
|
749
|
+
// rows so the scrollbar still describes the whole collection. Indices stay
|
|
750
|
+
// ABSOLUTE: selection, shift-range, and activateRow all address `rows`, and
|
|
751
|
+
// renumbering the slice would select the wrong record.
|
|
752
|
+
//
|
|
753
|
+
// `virtualize` is a request, not a guarantee. `resolveRowWindow` derives
|
|
754
|
+
// the whole scroll range from `rows.length * rowHeight`, so it can only
|
|
755
|
+
// describe a collection of uniform, fixed-height rows. An expanded row
|
|
756
|
+
// renders a second `<tr part="detail-row">` the arithmetic knows nothing
|
|
757
|
+
// about: the real content grows taller than the spacers claim, the scroll
|
|
758
|
+
// range no longer matches the scrollbar, and scrollTop stops mapping to the
|
|
759
|
+
// right absolute index — so a windowed table with expandable rows would
|
|
760
|
+
// scroll to the wrong record. Variable heights are a different algorithm (a
|
|
761
|
+
// cumulative offset index), not a flag on this one, so windowing is
|
|
762
|
+
// suppressed rather than approximated.
|
|
763
|
+
//
|
|
764
|
+
// The test is `expandable`, not "is anything expanded right now": deciding
|
|
765
|
+
// from transient UI state would flip windowing on and off mid-scroll, which
|
|
766
|
+
// jumps the viewport under the pointer. A collection that can expand never
|
|
767
|
+
// windows; `renderedWindow` returns null so a host can see that it didn't.
|
|
768
|
+
const virtualizing = this.virtualize && !expandable;
|
|
769
|
+
const rowWindow = virtualizing ? this.currentWindow() : null;
|
|
770
|
+
this.windowing = virtualizing;
|
|
771
|
+
this.lastWindow = rowWindow;
|
|
772
|
+
const visibleRows = rowWindow
|
|
773
|
+
? rows.slice(rowWindow.startIndex, rowWindow.endIndex)
|
|
774
|
+
: rows;
|
|
775
|
+
const indexOffset = rowWindow?.startIndex ?? 0;
|
|
776
|
+
// The tab stop belongs to the first row that is actually rendered; row 0
|
|
777
|
+
// may be scrolled far out of the window.
|
|
778
|
+
const tabbableIndex = indexOffset;
|
|
779
|
+
const spacer = (height, position) => height > 0
|
|
780
|
+
? `<tr part="spacer" data-position="${position}" aria-hidden="true"><td colspan="${String(columnCount)}" style="height:${String(height)}px;padding:0;border:0;"></td></tr>`
|
|
781
|
+
: "";
|
|
782
|
+
this.bodyEl.innerHTML =
|
|
783
|
+
(rowWindow ? spacer(rowWindow.paddingTop, "top") : "") +
|
|
784
|
+
visibleRows
|
|
785
|
+
.map((row, offset) => {
|
|
786
|
+
const index = offset + indexOffset;
|
|
787
|
+
const selected = this.selected.has(row.id);
|
|
788
|
+
const cellRole = selectable ? ' role="gridcell"' : ' role="cell"';
|
|
789
|
+
const rowAttrs = selectable
|
|
790
|
+
? ` part="row" role="row" tabindex="${index === tabbableIndex ? "0" : "-1"}" aria-selected="${selected}"`
|
|
791
|
+
: ' part="row" role="row"';
|
|
792
|
+
const expanded = this.expandedIds.has(row.id);
|
|
793
|
+
const expanderCell = expandable
|
|
794
|
+
? `<td${cellRole} part="expander-cell">${typeof row.detail === "string"
|
|
795
|
+
? `<button type="button" part="expander" data-row-id="${escapeHtml(row.id)}" aria-expanded="${String(expanded)}" aria-label="${expanded ? "Hide" : "Show"} details for row ${escapeHtml(row.id)}"></button>`
|
|
796
|
+
: ""}</td>`
|
|
797
|
+
: "";
|
|
798
|
+
const cells = columns
|
|
799
|
+
.map((column, columnIndex) => {
|
|
800
|
+
const align = alignAttr(column.align);
|
|
801
|
+
const value = this.cellValue(row, column, columnIndex);
|
|
802
|
+
return `<td${align}${cellRole} data-label="${escapeHtml(column.label)}">${this.cellMarkup(value)}</td>`;
|
|
803
|
+
})
|
|
804
|
+
.join("");
|
|
805
|
+
const rowMarkup = `<tr${rowAttrs} data-index="${index}" data-id="${escapeHtml(row.id)}">${expanderCell}${cells}</tr>`;
|
|
806
|
+
// The detail row renders only while expanded, keeping the roving
|
|
807
|
+
// tabindex and shift-range math on visible [part="row"] rows only.
|
|
808
|
+
const detailRow = typeof row.detail === "string" && expanded
|
|
809
|
+
? `<tr part="detail-row" role="row" data-for="${escapeHtml(row.id)}"><td${cellRole} colspan="${String(columnCount)}">${escapeHtml(row.detail)}<slot name="detail-${escapeHtml(row.id)}"></slot></td></tr>`
|
|
810
|
+
: "";
|
|
811
|
+
return rowMarkup + detailRow;
|
|
812
|
+
})
|
|
813
|
+
.join("") +
|
|
814
|
+
(rowWindow ? spacer(rowWindow.paddingBottom, "bottom") : "");
|
|
815
|
+
if (virtualizing) {
|
|
816
|
+
this.measureRowHeight();
|
|
817
|
+
}
|
|
315
818
|
}
|
|
316
819
|
}
|
|
317
820
|
Table.register();
|