@jxsuite/ui 0.2.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/ui",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "The Jx UI kit: interface elements authored as Jx documents, with their theme and icons",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -106,8 +106,30 @@ export function syncListbox(host: HTMLElement, active: string): OptionElement |
106
106
  */
107
107
  export function mountListbox(state: ListboxState, host: HTMLElement): void {
108
108
  syncListbox(host, String(state.active ?? ""));
109
+ /* One sync per frame, not one per mutation record. The palette opens by constructing its row
110
+ set: dozens of `childList` records in one task, and a sync per record is a querySelectorAll
111
+ walk plus a flush-baiting read each — the DevTools timeline billed every one of them. The
112
+ LAST record of a task settles the same active id the task started with (a filter's rows land
113
+ whole, and a caret move never changes rows), so reading `active` at frame time is the same
114
+ answer once, with one style pass for the burst instead of one per record. */
115
+ let frameId: number | null = null;
116
+ const scheduleSync = (): void => {
117
+ if (frameId !== null) {
118
+ return;
119
+ }
120
+ frameId = requestAnimationFrame(() => {
121
+ frameId = null;
122
+ if (host.isConnected) {
123
+ syncListbox(host, String(state.active ?? ""));
124
+ }
125
+ });
126
+ };
109
127
  const observer = new MutationObserver(() => {
110
- syncListbox(host, String(state.active ?? ""));
128
+ if (typeof requestAnimationFrame === "function") {
129
+ scheduleSync();
130
+ } else {
131
+ syncListbox(host, String(state.active ?? ""));
132
+ }
111
133
  });
112
134
  observer.observe(host, {
113
135
  attributeFilter: ["data-active"],
@@ -202,11 +202,37 @@ function coordOf(vertical: boolean, event: { clientX: number; clientY: number })
202
202
  * realm cannot see through — walking past it costs a frame's work once per gesture and is the only
203
203
  * thing that makes the pane grid's two nested `display: contents` rows measurable at all.
204
204
  *
205
+ * The ancestor that resolved as the track is MEMOIZED per host and re-read per call. Only its
206
+ * identity is cached; the length is measured fresh every time, so a window resize answers the new
207
+ * track width with no listener anywhere. Re-measuring one element's box instead of walking with a
208
+ * computed-style test per ancestor is what profiling pointed at: every gesture during a shell rep
209
+ * forced the dirty-tree flush and the DevTools timeline charged the recalc to the read, five times
210
+ * per boot with a document open. The cache re-walks when the remembered track no longer contains
211
+ * the host (reparenting, teardown) — containment and connectivity are node walks, not layout — and
212
+ * when the remembered track measures zero, because an unlaid-out track resolving later (the first
213
+ * frame after a pane opens) is the case the walk exists for.
214
+ *
205
215
  * @param host The element.
206
216
  * @param vertical Whether the drag is along x.
207
217
  * @returns The length in pixels, or 0 when nothing above the splitter has one.
208
218
  */
209
- function trackOf(host: HTMLElement, vertical: boolean): number {
219
+ const trackCache = new WeakMap<HTMLElement, { element: HTMLElement; vertical: boolean }>();
220
+
221
+ export function trackOf(host: HTMLElement, vertical: boolean): number {
222
+ const cached = trackCache.get(host);
223
+ if (
224
+ cached &&
225
+ cached.vertical === vertical &&
226
+ cached.element.isConnected &&
227
+ host.isConnected &&
228
+ cached.element.contains(host)
229
+ ) {
230
+ const box = cached.element.getBoundingClientRect();
231
+ const length = vertical ? box.width : box.height;
232
+ if (length > 0) {
233
+ return length;
234
+ }
235
+ }
210
236
  for (let node = host.parentElement; node; node = node.parentElement) {
211
237
  if (globalThis.getComputedStyle(node).display === "contents") {
212
238
  continue;
@@ -214,6 +240,7 @@ function trackOf(host: HTMLElement, vertical: boolean): number {
214
240
  const box = node.getBoundingClientRect();
215
241
  const length = vertical ? box.width : box.height;
216
242
  if (length > 0) {
243
+ trackCache.set(host, { element: node, vertical });
217
244
  return length;
218
245
  }
219
246
  }