@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
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { groupShortcutCommands, isCommandDescriptorRecord, splitShortcutKeys, } from "./command-types.js";
|
|
2
|
+
import { BaseElement } from "../../core/index.js";
|
|
3
|
+
import { FocusRestore, trapTabKey } from "../../foundations/a11y/index.js";
|
|
4
|
+
import { boeOverlay, boePanel, boeRadius } from "../../foundations/geometry/index.js";
|
|
5
|
+
const DEFAULT_TAG_NAME = "box-shortcuts-overlay";
|
|
6
|
+
const escapeHtml = (value) => value
|
|
7
|
+
.replaceAll("&", "&")
|
|
8
|
+
.replaceAll("<", "<")
|
|
9
|
+
.replaceAll(">", ">")
|
|
10
|
+
.replaceAll('"', """)
|
|
11
|
+
.replaceAll("'", "'");
|
|
12
|
+
/** Editable targets where a bare-character hotkey must never fire. */
|
|
13
|
+
const isTypingTarget = (target) => {
|
|
14
|
+
const element = target;
|
|
15
|
+
if (!element || typeof element.tagName !== "string") {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (element.isContentEditable) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
const tag = element.tagName;
|
|
22
|
+
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The element the key was actually typed into. `event.target` is retargeted to
|
|
26
|
+
* the shadow host once an event crosses a shadow boundary, and this library
|
|
27
|
+
* wraps native controls in shadow DOM — so reading `target` on a document
|
|
28
|
+
* listener sees `box-text-field`, not the `input` inside it.
|
|
29
|
+
*/
|
|
30
|
+
const keyOrigin = (event) => event.composedPath()[0] ?? event.target;
|
|
31
|
+
const elementStyles = `
|
|
32
|
+
[hidden] {
|
|
33
|
+
display: none !important;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
:host {
|
|
37
|
+
display: contents;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
[part="backdrop"] {
|
|
41
|
+
position: fixed;
|
|
42
|
+
inset: 0;
|
|
43
|
+
display: flex;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
align-items: center;
|
|
46
|
+
padding: 1rem;
|
|
47
|
+
background: ${boeOverlay.modalBackdrop};
|
|
48
|
+
z-index: 1000;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
[part="sheet"] {
|
|
52
|
+
inline-size: min(34rem, 100%);
|
|
53
|
+
max-block-size: 80vh;
|
|
54
|
+
display: grid;
|
|
55
|
+
grid-template-rows: auto 1fr;
|
|
56
|
+
border-radius: ${boeOverlay.modalRadius};
|
|
57
|
+
border: ${boePanel.border};
|
|
58
|
+
background: ${boePanel.background};
|
|
59
|
+
box-shadow: ${boeOverlay.shadow};
|
|
60
|
+
overflow: hidden;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
[part="header"] {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 0.5rem;
|
|
67
|
+
padding: 0.85rem 1rem;
|
|
68
|
+
border-block-end: 1px solid color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 70%, transparent);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
[part="title"] {
|
|
72
|
+
margin: 0;
|
|
73
|
+
font: inherit;
|
|
74
|
+
font-size: 1rem;
|
|
75
|
+
font-weight: 700;
|
|
76
|
+
color: var(--boe-token-text-text, #1f1e1b);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
[part="close"] {
|
|
80
|
+
margin-inline-start: auto;
|
|
81
|
+
appearance: none;
|
|
82
|
+
font: inherit;
|
|
83
|
+
font-size: 0.8rem;
|
|
84
|
+
padding: 0.25rem 0.55rem;
|
|
85
|
+
border: 1px solid var(--boe-token-stroke-stroke, #e8e8e8);
|
|
86
|
+
border-radius: ${boeRadius.control};
|
|
87
|
+
background: var(--boe-token-surface-surface, #ffffff);
|
|
88
|
+
color: var(--boe-token-text-text, #222222);
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
[part="close"]:hover {
|
|
93
|
+
background: var(--boe-token-surface-surface-hover, #f4f4f4);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
[part="groups"] {
|
|
97
|
+
margin: 0;
|
|
98
|
+
padding: 0.6rem 1rem 1rem;
|
|
99
|
+
list-style: none;
|
|
100
|
+
overflow-y: auto;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
[part="group-label"] {
|
|
104
|
+
display: block;
|
|
105
|
+
padding-block: 0.7rem 0.3rem;
|
|
106
|
+
font-size: 0.7rem;
|
|
107
|
+
font-weight: 700;
|
|
108
|
+
letter-spacing: 0.08em;
|
|
109
|
+
text-transform: uppercase;
|
|
110
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
[part="rows"] {
|
|
114
|
+
margin: 0;
|
|
115
|
+
padding: 0;
|
|
116
|
+
list-style: none;
|
|
117
|
+
display: grid;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
[part="row"] {
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: baseline;
|
|
123
|
+
gap: 0.75rem;
|
|
124
|
+
padding: 0.35rem 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
[part="row-label"] {
|
|
128
|
+
color: var(--boe-token-text-text, #1f1e1b);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
[part="keys"] {
|
|
132
|
+
margin-inline-start: auto;
|
|
133
|
+
display: inline-flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: 0.2rem;
|
|
136
|
+
white-space: nowrap;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
[part="key"] {
|
|
140
|
+
min-inline-size: 1.4rem;
|
|
141
|
+
padding: 0.1rem 0.35rem;
|
|
142
|
+
border: 1px solid var(--boe-token-stroke-stroke, #e8e8e8);
|
|
143
|
+
border-block-end-width: 2px;
|
|
144
|
+
border-radius: ${boeRadius.size};
|
|
145
|
+
background: var(--boe-token-surface-surface-hover, #f4f4f4);
|
|
146
|
+
font-family: ui-monospace, monospace;
|
|
147
|
+
font-size: 0.72rem;
|
|
148
|
+
text-align: center;
|
|
149
|
+
color: var(--boe-token-text-text, #1f1e1b);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
[part="key-separator"] {
|
|
153
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
154
|
+
font-size: 0.7rem;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
[part="empty"] {
|
|
158
|
+
padding: 1.4rem 1rem;
|
|
159
|
+
text-align: center;
|
|
160
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
[part="close"]:focus-visible {
|
|
164
|
+
outline: none;
|
|
165
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 18%, transparent);
|
|
166
|
+
}
|
|
167
|
+
`;
|
|
168
|
+
/**
|
|
169
|
+
* The keyboard shortcuts sheet that makes keyboard-first real — the pair to
|
|
170
|
+
* `box-command-palette`.
|
|
171
|
+
*
|
|
172
|
+
* It reads the *same* `CommandDescriptor[]` the palette does and lists only
|
|
173
|
+
* the commands that declare a `shortcut`. One catalogue driving both surfaces
|
|
174
|
+
* is the point: a shortcut cannot end up documented but unreachable, or
|
|
175
|
+
* reachable but undocumented, because there is only one place to add it.
|
|
176
|
+
*
|
|
177
|
+
* Keys render as `<kbd>` elements rather than a run of text, so the markup
|
|
178
|
+
* says what it means.
|
|
179
|
+
*/
|
|
180
|
+
export class ShortcutsOverlay extends BaseElement {
|
|
181
|
+
static tagName = DEFAULT_TAG_NAME;
|
|
182
|
+
static get observedAttributes() {
|
|
183
|
+
return ["commands", "heading", "hotkey", "open"];
|
|
184
|
+
}
|
|
185
|
+
hostEl;
|
|
186
|
+
focusRestore = new FocusRestore();
|
|
187
|
+
wasOpen = false;
|
|
188
|
+
commandsRaw = null;
|
|
189
|
+
commandsCache = [];
|
|
190
|
+
onDocumentKeydown = (event) => {
|
|
191
|
+
if (this.open) {
|
|
192
|
+
// Escape closes from anywhere. Focus can end up outside the sheet — the
|
|
193
|
+
// browser chrome, a host that moves it — and a modal that only closes
|
|
194
|
+
// while it still holds focus is a trap. The sheet's own handler runs
|
|
195
|
+
// first and clears `open`, so this never dismisses twice.
|
|
196
|
+
if (event.key === "Escape") {
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
this.dismiss();
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const hotkey = this.hotkey;
|
|
203
|
+
if (!hotkey) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// A bare-character hotkey must never fire while someone is typing —
|
|
207
|
+
// "?" is a legitimate character in every text field on the page.
|
|
208
|
+
if (isTypingTarget(keyOrigin(event))) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (event.metaKey || event.ctrlKey || event.altKey) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (event.key === hotkey) {
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
this.open = true;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
get open() {
|
|
220
|
+
return this.hasAttribute("open");
|
|
221
|
+
}
|
|
222
|
+
set open(value) {
|
|
223
|
+
this.toggleAttribute("open", value);
|
|
224
|
+
}
|
|
225
|
+
get heading() {
|
|
226
|
+
return this.getAttribute("heading") ?? "Keyboard shortcuts";
|
|
227
|
+
}
|
|
228
|
+
set heading(value) {
|
|
229
|
+
this.setAttribute("heading", value);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Character that opens the sheet. Defaults to `?`, the convention. Matched
|
|
233
|
+
* against the produced character, so the Shift needed to type it is already
|
|
234
|
+
* accounted for. Set empty to let the host own the trigger.
|
|
235
|
+
*/
|
|
236
|
+
get hotkey() {
|
|
237
|
+
const raw = this.getAttribute("hotkey");
|
|
238
|
+
return raw === null ? "?" : raw;
|
|
239
|
+
}
|
|
240
|
+
set hotkey(value) {
|
|
241
|
+
this.setAttribute("hotkey", value);
|
|
242
|
+
}
|
|
243
|
+
/** The same catalogue `box-command-palette` takes; validated per record. */
|
|
244
|
+
get commands() {
|
|
245
|
+
const raw = this.getAttribute("commands");
|
|
246
|
+
if (!raw) {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
if (raw !== this.commandsRaw) {
|
|
250
|
+
this.commandsRaw = raw;
|
|
251
|
+
try {
|
|
252
|
+
const parsed = JSON.parse(raw);
|
|
253
|
+
this.commandsCache =
|
|
254
|
+
Array.isArray(parsed) && parsed.every(isCommandDescriptorRecord)
|
|
255
|
+
? parsed
|
|
256
|
+
: [];
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
this.commandsCache = [];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return [...this.commandsCache];
|
|
263
|
+
}
|
|
264
|
+
set commands(value) {
|
|
265
|
+
if (value.length) {
|
|
266
|
+
this.setAttribute("commands", JSON.stringify(value));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
this.removeAttribute("commands");
|
|
270
|
+
}
|
|
271
|
+
/** Only the commands that declare a shortcut, in catalogue order. */
|
|
272
|
+
get documentedCommands() {
|
|
273
|
+
return groupShortcutCommands(this.commands).flatMap(group => group.commands);
|
|
274
|
+
}
|
|
275
|
+
show() {
|
|
276
|
+
this.open = true;
|
|
277
|
+
}
|
|
278
|
+
hide() {
|
|
279
|
+
this.open = false;
|
|
280
|
+
}
|
|
281
|
+
connectedCallback() {
|
|
282
|
+
super.connectedCallback();
|
|
283
|
+
document.addEventListener("keydown", this.onDocumentKeydown);
|
|
284
|
+
}
|
|
285
|
+
disconnectedCallback() {
|
|
286
|
+
document.removeEventListener("keydown", this.onDocumentKeydown);
|
|
287
|
+
}
|
|
288
|
+
dismiss() {
|
|
289
|
+
this.dispatchEvent(new CustomEvent("dismissed", { bubbles: true, composed: true }));
|
|
290
|
+
this.open = false;
|
|
291
|
+
}
|
|
292
|
+
renderTemplate() {
|
|
293
|
+
if (!this.shadowRoot) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
this.shadowRoot.innerHTML = `<style>${elementStyles}</style><div part="host"></div>`;
|
|
297
|
+
this.hostEl = this.shadowRoot.querySelector('[part="host"]');
|
|
298
|
+
}
|
|
299
|
+
setupListeners() {
|
|
300
|
+
this.hostEl.addEventListener("pointerdown", event => {
|
|
301
|
+
const target = event.target;
|
|
302
|
+
if (target.closest('[part="sheet"]')) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (target.closest('[part="backdrop"]')) {
|
|
306
|
+
this.dismiss();
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
this.hostEl.addEventListener("click", event => {
|
|
310
|
+
if (event.target.closest('[part="close"]')) {
|
|
311
|
+
this.dismiss();
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
this.hostEl.addEventListener("keydown", event => {
|
|
315
|
+
const keyboardEvent = event;
|
|
316
|
+
const sheet = this.hostEl.querySelector('[part="sheet"]');
|
|
317
|
+
if (!sheet) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (keyboardEvent.key === "Escape") {
|
|
321
|
+
keyboardEvent.preventDefault();
|
|
322
|
+
this.dismiss();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (keyboardEvent.key === "Tab") {
|
|
326
|
+
trapTabKey(keyboardEvent, sheet);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
update() {
|
|
331
|
+
if (!this.hostEl) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (!this.open) {
|
|
335
|
+
const wasOpen = this.wasOpen;
|
|
336
|
+
this.hostEl.innerHTML = "";
|
|
337
|
+
this.wasOpen = false;
|
|
338
|
+
if (wasOpen) {
|
|
339
|
+
this.focusRestore.restore();
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const justOpened = !this.wasOpen;
|
|
344
|
+
this.wasOpen = true;
|
|
345
|
+
if (justOpened) {
|
|
346
|
+
this.focusRestore.capture();
|
|
347
|
+
}
|
|
348
|
+
const groups = groupShortcutCommands(this.commands);
|
|
349
|
+
const sections = groups
|
|
350
|
+
.map(group => {
|
|
351
|
+
const rows = group.commands
|
|
352
|
+
.map(command => {
|
|
353
|
+
const keys = splitShortcutKeys(command.shortcut ?? "")
|
|
354
|
+
.map(key => `<kbd part="key">${escapeHtml(key)}</kbd>`)
|
|
355
|
+
.join(`<span part="key-separator" aria-hidden="true">+</span>`);
|
|
356
|
+
return `
|
|
357
|
+
<li part="row" data-command-id="${escapeHtml(command.id)}">
|
|
358
|
+
<span part="row-label">${escapeHtml(command.label)}</span>
|
|
359
|
+
<span part="keys" aria-label="${escapeHtml(command.shortcut ?? "")}">${keys}</span>
|
|
360
|
+
</li>
|
|
361
|
+
`;
|
|
362
|
+
})
|
|
363
|
+
.join("");
|
|
364
|
+
return `<li part="group" data-group-key="${escapeHtml(group.key)}"><span part="group-label">${escapeHtml(group.label)}</span><ul part="rows" role="list">${rows}</ul></li>`;
|
|
365
|
+
})
|
|
366
|
+
.join("");
|
|
367
|
+
this.hostEl.innerHTML = `
|
|
368
|
+
<div part="backdrop">
|
|
369
|
+
<div part="sheet" role="dialog" aria-modal="true" aria-label="${escapeHtml(this.heading)}">
|
|
370
|
+
<div part="header">
|
|
371
|
+
<h2 part="title">${escapeHtml(this.heading)}</h2>
|
|
372
|
+
<button type="button" part="close">Close</button>
|
|
373
|
+
</div>
|
|
374
|
+
${groups.length
|
|
375
|
+
? `<ul part="groups" role="list">${sections}</ul>`
|
|
376
|
+
: `<div part="empty">No keyboard shortcuts are defined.</div>`}
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
`;
|
|
380
|
+
if (justOpened) {
|
|
381
|
+
this.hostEl.querySelector('[part="close"]')?.focus();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
ShortcutsOverlay.register();
|
|
@@ -51,7 +51,7 @@ const tooltipStyles = `
|
|
|
51
51
|
border-radius: ${boeRadius.xlarge};
|
|
52
52
|
border: 1px solid color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 18%, rgba(255, 255, 255, 0.08));
|
|
53
53
|
background: var(--boe-token-surface-tooltip-surface, #4e4e4e);
|
|
54
|
-
color:
|
|
54
|
+
color: #ffffff;
|
|
55
55
|
box-shadow: 0 16px 28px rgba(16, 24, 32, 0.18);
|
|
56
56
|
line-height: 1.45;
|
|
57
57
|
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -4,3 +4,7 @@ export type { EventMap, Unsubscribe } from "./event-emitter.js";
|
|
|
4
4
|
export { BaseElement } from "./element.js";
|
|
5
5
|
export { FORM_DESCRIPTION_ID, FORM_ERROR_MESSAGE_ID, FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDataFromNamedValues, formDataFromRange, formDescriptionMarkup, formErrorMessageMarkup, getMirroredFormValue, rangeFromFormValue, stringValuesFromFormValue, } from "./form-associated.js";
|
|
6
6
|
export type { FormValue } from "./form-associated.js";
|
|
7
|
+
export { resolveRowWindow, sameRowWindow } from "./virtualize.js";
|
|
8
|
+
export type { RowWindow, WindowMetrics } from "./virtualize.js";
|
|
9
|
+
export { buildOffsetIndex, offsetOfItem, resolveOffsetWindow } from "./offset-window.js";
|
|
10
|
+
export type { OffsetIndex, OffsetWindowMetrics } from "./offset-window.js";
|
package/dist/core/index.js
CHANGED
|
@@ -2,3 +2,5 @@ export { Controller } from "./controller.js";
|
|
|
2
2
|
export { TypedEventEmitter } from "./event-emitter.js";
|
|
3
3
|
export { BaseElement } from "./element.js";
|
|
4
4
|
export { FORM_DESCRIPTION_ID, FORM_ERROR_MESSAGE_ID, FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDataFromNamedValues, formDataFromRange, formDescriptionMarkup, formErrorMessageMarkup, getMirroredFormValue, rangeFromFormValue, stringValuesFromFormValue, } from "./form-associated.js";
|
|
5
|
+
export { resolveRowWindow, sameRowWindow } from "./virtualize.js";
|
|
6
|
+
export { buildOffsetIndex, offsetOfItem, resolveOffsetWindow } from "./offset-window.js";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Row windowing for lists whose rows are *not* all the same height.
|
|
3
|
+
*
|
|
4
|
+
* `resolveRowWindow` (./virtualize.ts) multiplies a row count by one row
|
|
5
|
+
* height. That is exact and O(1), and it is the right answer for a flat table
|
|
6
|
+
* of uniform rows — but it cannot describe a surface where the items differ:
|
|
7
|
+
* a grouped audit log alternates short headings with taller event rows, a
|
|
8
|
+
* collapsed group contributes only its heading, and an expanded detail row adds
|
|
9
|
+
* height the count knows nothing about. Feeding those to the fixed-height
|
|
10
|
+
* engine does not degrade gracefully; the spacers under-report the real height,
|
|
11
|
+
* the scroll range stops matching the scrollbar, and `scrollTop` maps to the
|
|
12
|
+
* wrong item. That is why `box-table` refuses to window expandable rows rather
|
|
13
|
+
* than approximating them.
|
|
14
|
+
*
|
|
15
|
+
* The general answer is a cumulative offset index: the running pixel offset of
|
|
16
|
+
* every item, built once, then binary-searched per scroll. Building is O(n) and
|
|
17
|
+
* searching is O(log n), which is the right split — heights change when the
|
|
18
|
+
* data or the collapse state changes, not on every scroll event.
|
|
19
|
+
*
|
|
20
|
+
* Heights are still *estimates* until something measures them. This engine
|
|
21
|
+
* takes whatever heights it is given and is exact with respect to them; keeping
|
|
22
|
+
* them honest is the caller's job, the same bargain `row-height` strikes.
|
|
23
|
+
*/
|
|
24
|
+
import type { RowWindow } from "./virtualize.js";
|
|
25
|
+
export interface OffsetIndex {
|
|
26
|
+
/**
|
|
27
|
+
* Running offsets, length `count + 1`. `offsets[i]` is the top of item `i`;
|
|
28
|
+
* `offsets[count]` is the total height, so the height of item `i` is
|
|
29
|
+
* `offsets[i + 1] - offsets[i]` with no special case for the last one.
|
|
30
|
+
*/
|
|
31
|
+
readonly offsets: readonly number[];
|
|
32
|
+
readonly count: number;
|
|
33
|
+
readonly totalHeight: number;
|
|
34
|
+
}
|
|
35
|
+
export interface OffsetWindowMetrics {
|
|
36
|
+
/** Height of the scrolling viewport, in pixels. */
|
|
37
|
+
viewportHeight: number;
|
|
38
|
+
/** Current scroll offset of the viewport, in pixels. */
|
|
39
|
+
scrollTop: number;
|
|
40
|
+
/** Extra items rendered above and below the visible band. Default 4. */
|
|
41
|
+
overscan?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The cumulative offset index for a list of item heights.
|
|
45
|
+
*
|
|
46
|
+
* A non-finite or negative height contributes zero rather than poisoning every
|
|
47
|
+
* subsequent offset with `NaN`. Heights arrive from measurement and from host
|
|
48
|
+
* estimates; one bad value should cost that item its space, not break the
|
|
49
|
+
* scrollbar for the whole collection.
|
|
50
|
+
*/
|
|
51
|
+
export declare const buildOffsetIndex: (heights: readonly number[]) => OffsetIndex;
|
|
52
|
+
/**
|
|
53
|
+
* The rendered slice for a scroll position, plus the spacer heights.
|
|
54
|
+
*
|
|
55
|
+
* Same contract as `resolveRowWindow`, so the two engines are interchangeable
|
|
56
|
+
* from a component's point of view:
|
|
57
|
+
* - `paddingTop + (height of the rendered items) + paddingBottom` always equals
|
|
58
|
+
* `totalHeight`, so the scroll range never drifts as items scroll by.
|
|
59
|
+
* - The window always covers the visible band; overscan only ever adds items.
|
|
60
|
+
* - Degenerate geometry yields an empty window rather than a guess.
|
|
61
|
+
*/
|
|
62
|
+
export declare const resolveOffsetWindow: (index: OffsetIndex, metrics: OffsetWindowMetrics) => RowWindow;
|
|
63
|
+
/**
|
|
64
|
+
* The pixel offset of an item, for scrolling it into view.
|
|
65
|
+
*
|
|
66
|
+
* Out-of-range indices clamp to the ends rather than returning `undefined`:
|
|
67
|
+
* callers are keyboard handlers reaching for "the last one", and a clamp is
|
|
68
|
+
* what they would write anyway.
|
|
69
|
+
*/
|
|
70
|
+
export declare const offsetOfItem: (index: OffsetIndex, item: number) => number;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Row windowing for lists whose rows are *not* all the same height.
|
|
3
|
+
*
|
|
4
|
+
* `resolveRowWindow` (./virtualize.ts) multiplies a row count by one row
|
|
5
|
+
* height. That is exact and O(1), and it is the right answer for a flat table
|
|
6
|
+
* of uniform rows — but it cannot describe a surface where the items differ:
|
|
7
|
+
* a grouped audit log alternates short headings with taller event rows, a
|
|
8
|
+
* collapsed group contributes only its heading, and an expanded detail row adds
|
|
9
|
+
* height the count knows nothing about. Feeding those to the fixed-height
|
|
10
|
+
* engine does not degrade gracefully; the spacers under-report the real height,
|
|
11
|
+
* the scroll range stops matching the scrollbar, and `scrollTop` maps to the
|
|
12
|
+
* wrong item. That is why `box-table` refuses to window expandable rows rather
|
|
13
|
+
* than approximating them.
|
|
14
|
+
*
|
|
15
|
+
* The general answer is a cumulative offset index: the running pixel offset of
|
|
16
|
+
* every item, built once, then binary-searched per scroll. Building is O(n) and
|
|
17
|
+
* searching is O(log n), which is the right split — heights change when the
|
|
18
|
+
* data or the collapse state changes, not on every scroll event.
|
|
19
|
+
*
|
|
20
|
+
* Heights are still *estimates* until something measures them. This engine
|
|
21
|
+
* takes whatever heights it is given and is exact with respect to them; keeping
|
|
22
|
+
* them honest is the caller's job, the same bargain `row-height` strikes.
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_OVERSCAN = 4;
|
|
25
|
+
const EMPTY_INDEX = { offsets: [0], count: 0, totalHeight: 0 };
|
|
26
|
+
/**
|
|
27
|
+
* The cumulative offset index for a list of item heights.
|
|
28
|
+
*
|
|
29
|
+
* A non-finite or negative height contributes zero rather than poisoning every
|
|
30
|
+
* subsequent offset with `NaN`. Heights arrive from measurement and from host
|
|
31
|
+
* estimates; one bad value should cost that item its space, not break the
|
|
32
|
+
* scrollbar for the whole collection.
|
|
33
|
+
*/
|
|
34
|
+
export const buildOffsetIndex = (heights) => {
|
|
35
|
+
if (heights.length === 0)
|
|
36
|
+
return EMPTY_INDEX;
|
|
37
|
+
const offsets = new Array(heights.length + 1);
|
|
38
|
+
offsets[0] = 0;
|
|
39
|
+
let running = 0;
|
|
40
|
+
for (let index = 0; index < heights.length; index++) {
|
|
41
|
+
const height = heights[index] ?? 0;
|
|
42
|
+
running += Number.isFinite(height) && height > 0 ? height : 0;
|
|
43
|
+
offsets[index + 1] = running;
|
|
44
|
+
}
|
|
45
|
+
return { offsets, count: heights.length, totalHeight: running };
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The index of the last item whose top is at or before `offset`.
|
|
49
|
+
*
|
|
50
|
+
* Binary search rather than a scan: this runs per scroll frame, and a linear
|
|
51
|
+
* walk over ten thousand offsets is the cost windowing exists to avoid.
|
|
52
|
+
* Zero-height items make offsets non-strictly-increasing, so ties resolve to
|
|
53
|
+
* the *first* of a run — the alternative is a window that starts inside a
|
|
54
|
+
* cluster of collapsed items and leaves the one above it unrendered.
|
|
55
|
+
*/
|
|
56
|
+
const findItemAt = (offsets, count, offset) => {
|
|
57
|
+
let low = 0;
|
|
58
|
+
let high = count - 1;
|
|
59
|
+
let found = 0;
|
|
60
|
+
while (low <= high) {
|
|
61
|
+
const mid = (low + high) >> 1;
|
|
62
|
+
if ((offsets[mid] ?? 0) <= offset) {
|
|
63
|
+
found = mid;
|
|
64
|
+
low = mid + 1;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
high = mid - 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Walk back over a run of equal offsets (zero-height items) so the window
|
|
71
|
+
// starts at the first item sharing this offset, not an arbitrary one.
|
|
72
|
+
const at = offsets[found] ?? 0;
|
|
73
|
+
while (found > 0 && (offsets[found - 1] ?? 0) === at)
|
|
74
|
+
found--;
|
|
75
|
+
return found;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* The rendered slice for a scroll position, plus the spacer heights.
|
|
79
|
+
*
|
|
80
|
+
* Same contract as `resolveRowWindow`, so the two engines are interchangeable
|
|
81
|
+
* from a component's point of view:
|
|
82
|
+
* - `paddingTop + (height of the rendered items) + paddingBottom` always equals
|
|
83
|
+
* `totalHeight`, so the scroll range never drifts as items scroll by.
|
|
84
|
+
* - The window always covers the visible band; overscan only ever adds items.
|
|
85
|
+
* - Degenerate geometry yields an empty window rather than a guess.
|
|
86
|
+
*/
|
|
87
|
+
export const resolveOffsetWindow = (index, metrics) => {
|
|
88
|
+
const { offsets, count, totalHeight } = index;
|
|
89
|
+
const overscan = Math.max(0, Math.floor(metrics.overscan ?? DEFAULT_OVERSCAN));
|
|
90
|
+
if (count <= 0 || totalHeight <= 0) {
|
|
91
|
+
return { startIndex: 0, endIndex: 0, paddingTop: 0, paddingBottom: 0, totalHeight };
|
|
92
|
+
}
|
|
93
|
+
if (metrics.viewportHeight <= 0) {
|
|
94
|
+
// An unmeasured viewport is the normal state during first paint and under
|
|
95
|
+
// jsdom. Rendering everything there would defeat the purpose exactly when
|
|
96
|
+
// it matters most.
|
|
97
|
+
return { startIndex: 0, endIndex: 0, paddingTop: 0, paddingBottom: totalHeight, totalHeight };
|
|
98
|
+
}
|
|
99
|
+
// Clamp a bounced or past-the-end scrollTop: rubber-banding reports both, and
|
|
100
|
+
// neither may produce a window outside the collection.
|
|
101
|
+
const maxScroll = Math.max(0, totalHeight - metrics.viewportHeight);
|
|
102
|
+
const top = Math.min(Math.max(0, metrics.scrollTop), maxScroll);
|
|
103
|
+
const bottom = top + metrics.viewportHeight;
|
|
104
|
+
const firstVisible = findItemAt(offsets, count, top);
|
|
105
|
+
// Last item that starts before the viewport's bottom edge. Scanning forward
|
|
106
|
+
// from firstVisible is bounded by what fits on screen plus overscan, which is
|
|
107
|
+
// the point of windowing — no walk over the whole collection.
|
|
108
|
+
let lastVisible = firstVisible;
|
|
109
|
+
while (lastVisible + 1 < count && (offsets[lastVisible + 1] ?? 0) < bottom) {
|
|
110
|
+
lastVisible++;
|
|
111
|
+
}
|
|
112
|
+
const startIndex = Math.max(0, firstVisible - overscan);
|
|
113
|
+
const endIndex = Math.min(count, lastVisible + 1 + overscan);
|
|
114
|
+
return {
|
|
115
|
+
startIndex,
|
|
116
|
+
endIndex,
|
|
117
|
+
paddingTop: offsets[startIndex] ?? 0,
|
|
118
|
+
paddingBottom: totalHeight - (offsets[endIndex] ?? totalHeight),
|
|
119
|
+
totalHeight,
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* The pixel offset of an item, for scrolling it into view.
|
|
124
|
+
*
|
|
125
|
+
* Out-of-range indices clamp to the ends rather than returning `undefined`:
|
|
126
|
+
* callers are keyboard handlers reaching for "the last one", and a clamp is
|
|
127
|
+
* what they would write anyway.
|
|
128
|
+
*/
|
|
129
|
+
export const offsetOfItem = (index, item) => {
|
|
130
|
+
if (index.count <= 0)
|
|
131
|
+
return 0;
|
|
132
|
+
const clamped = Math.min(Math.max(0, Math.floor(item)), index.count);
|
|
133
|
+
return index.offsets[clamped] ?? 0;
|
|
134
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Row windowing for long lists.
|
|
3
|
+
*
|
|
4
|
+
* A ten-thousand-row audit log rendered whole is ten thousand DOM rows: the
|
|
5
|
+
* first paint stalls, every subsequent patch walks the lot, and the browser
|
|
6
|
+
* holds layout boxes nobody will ever scroll to. Windowing renders only the
|
|
7
|
+
* rows near the viewport and pads the rest with spacer height, so the scrollbar
|
|
8
|
+
* still describes the whole collection.
|
|
9
|
+
*
|
|
10
|
+
* The arithmetic lives here, pure, for the usual reason: jsdom has no layout,
|
|
11
|
+
* so a DOM-level test of a windowed surface can only ever observe zeroes. It is
|
|
12
|
+
* shared rather than per-component so `box-table`, `box-audit-log`, and
|
|
13
|
+
* `box-tree-grid` cannot drift into three different definitions of "near".
|
|
14
|
+
*
|
|
15
|
+
* Fixed row height only. Measured/variable heights are a different algorithm
|
|
16
|
+
* (a cumulative offset index that reflows as rows are measured); pretending one
|
|
17
|
+
* function covers both is how windowing engines end up subtly wrong at the
|
|
18
|
+
* boundaries.
|
|
19
|
+
*/
|
|
20
|
+
export interface WindowMetrics {
|
|
21
|
+
/** Total rows in the collection, not the rendered subset. */
|
|
22
|
+
totalRows: number;
|
|
23
|
+
/** Fixed height of one row, in pixels. */
|
|
24
|
+
rowHeight: number;
|
|
25
|
+
/** Height of the scrolling viewport, in pixels. */
|
|
26
|
+
viewportHeight: number;
|
|
27
|
+
/** Current scroll offset of the viewport, in pixels. */
|
|
28
|
+
scrollTop: number;
|
|
29
|
+
/**
|
|
30
|
+
* Extra rows rendered above and below the visible band, so a fast scroll
|
|
31
|
+
* does not expose blank space before the next frame. Default 4.
|
|
32
|
+
*/
|
|
33
|
+
overscan?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface RowWindow {
|
|
36
|
+
/** First rendered row index, inclusive. */
|
|
37
|
+
startIndex: number;
|
|
38
|
+
/** Last rendered row index, exclusive — safe for `slice(startIndex, endIndex)`. */
|
|
39
|
+
endIndex: number;
|
|
40
|
+
/** Spacer height above the rendered rows, in pixels. */
|
|
41
|
+
paddingTop: number;
|
|
42
|
+
/** Spacer height below the rendered rows, in pixels. */
|
|
43
|
+
paddingBottom: number;
|
|
44
|
+
/** Height of the whole collection, for the scroll range. */
|
|
45
|
+
totalHeight: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The rendered slice for a scroll position, plus the spacer heights that keep
|
|
49
|
+
* the scrollbar honest.
|
|
50
|
+
*
|
|
51
|
+
* Guarantees callers can rely on:
|
|
52
|
+
* - `paddingTop + (endIndex - startIndex) * rowHeight + paddingBottom` always
|
|
53
|
+
* equals `totalHeight`, so the scroll range never drifts as rows scroll by —
|
|
54
|
+
* a windowed list whose height wobbles makes the scrollbar jump under the
|
|
55
|
+
* pointer.
|
|
56
|
+
* - The window always covers the visible band; overscan only ever adds rows.
|
|
57
|
+
* - Degenerate geometry (no rows, unmeasured viewport, non-positive row height)
|
|
58
|
+
* yields an empty window rather than a guess. An unmeasured viewport is the
|
|
59
|
+
* normal state during first paint and under jsdom, and rendering *everything*
|
|
60
|
+
* there would defeat the purpose at exactly the moment it matters most.
|
|
61
|
+
*/
|
|
62
|
+
export declare const resolveRowWindow: (metrics: WindowMetrics) => RowWindow;
|
|
63
|
+
/**
|
|
64
|
+
* Whether a scroll moved far enough to need a re-render.
|
|
65
|
+
*
|
|
66
|
+
* Scroll fires continuously; re-rendering on every event would rebuild the row
|
|
67
|
+
* markup dozens of times per second to show the same rows. Comparing the
|
|
68
|
+
* resolved window is cheaper and exact — the render is needed precisely when
|
|
69
|
+
* the slice changes.
|
|
70
|
+
*/
|
|
71
|
+
export declare const sameRowWindow: (a: RowWindow, b: RowWindow) => boolean;
|