@uniflowed/ui 0.0.0-alpha.40 → 0.0.0-alpha.42

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.
@@ -100,7 +100,7 @@ export type MenuState = {|
100
100
  * lands on the last item, and re-rendering the trigger to say so would be a
101
101
  * render whose only purpose is to carry a message to an effect.
102
102
  */
103
- readonly pendingFocus: { current: "first" | "last" | null },
103
+ readonly pendingFocusRef: { current: "first" | "last" | null },
104
104
  /** The menu this one hangs off, or null for the outermost. */
105
105
  readonly parent: MenuState | null,
106
106
  /**
@@ -207,7 +207,7 @@ export component MenuLevel(
207
207
  const base = useId();
208
208
  const [isOpen, setOpen] = useControlled(open, defaultOpen, onOpenChange);
209
209
  const triggerRef = useRef<HTMLElement | null>(null);
210
- const pendingFocus = useRef<"first" | "last" | null>(null);
210
+ const pendingFocusRef = useRef<"first" | "last" | null>(null);
211
211
  const [triggered, setTriggered] = useState(false);
212
212
 
213
213
  const state = useMemo(
@@ -216,7 +216,7 @@ export component MenuLevel(
216
216
  open: isOpen,
217
217
  setOpen,
218
218
  triggerRef,
219
- pendingFocus,
219
+ pendingFocusRef,
220
220
  parent,
221
221
  triggered,
222
222
  registerTrigger: setTriggered,
@@ -346,6 +346,8 @@ export hook useFirstItem(
346
346
  false,
347
347
  set.skipDisabled,
348
348
  );
349
+ // The first item is only knowable after the caller's items are in the DOM.
350
+ // uf-lint-disable-next-line react-compiler/set-state-in-effect
349
351
  setFirst(landing?.id ?? null);
350
352
  });
351
353
 
package/menu.js CHANGED
@@ -243,10 +243,14 @@ export component MenuTrigger(children: React.Node, render?: RenderProp, ...rest:
243
243
  return;
244
244
  }
245
245
  event.preventDefault();
246
- menu.pendingFocus.current = end;
246
+ // This is an instruction for the menu body after the opening commit.
247
+ // uf-lint-disable-next-line react-compiler/immutability
248
+ menu.pendingFocusRef.current = end;
247
249
  menu.setOpen(true);
248
250
  }),
249
251
  ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
252
+ // React calls callback refs during commit; the menu body reads the trigger later.
253
+ // uf-lint-disable-next-line react-compiler/immutability
250
254
  menu.triggerRef.current = element;
251
255
  }),
252
256
  });
@@ -296,7 +300,9 @@ export component MenuBody(
296
300
  // it re-ran on every parent render and re-took focus each time, dragging the
297
301
  // reader back to the first item while they were arrowing.
298
302
  const triggerRef = menu.triggerRef;
299
- const pendingFocus = menu.pendingFocus;
303
+ const pendingFocusRef = menu.pendingFocusRef;
304
+ // `parent` is menu-tree metadata; no ref value is read during render.
305
+ // uf-lint-disable-next-line react-compiler/refs
300
306
  const isRoot = menu.parent == null;
301
307
  const closeAll = useStableCallback(() => closeTree(menu));
302
308
  // A root menu drops from its button; a submenu comes out of the side of the
@@ -304,14 +310,20 @@ export component MenuBody(
304
310
  // be a parameter default because it is not a constant: it is the answer to
305
311
  // "is this the outermost menu", which only this component knows.
306
312
  const placement = side ?? (isRoot ? "bottom" : "inline-end");
313
+ // useAnchor accepts ref objects and reads them from layout/effects.
314
+ // uf-lint-disable-next-line react-compiler/refs
307
315
  const anchored = useAnchor({
308
316
  align,
309
317
  alignOffset,
310
318
  anchorRect: point,
319
+ // uf-lint-disable-next-line react-compiler/refs
311
320
  anchorRef: triggerRef,
312
321
  avoidCollisions,
313
322
  collisionPadding,
323
+ // `open` is menu-tree metadata; no ref value is read during render.
324
+ // uf-lint-disable-next-line react-compiler/refs
314
325
  open: menu.open,
326
+ // uf-lint-disable-next-line react-compiler/refs
315
327
  overlayRef: bodyRef,
316
328
  side: placement,
317
329
  sideOffset,
@@ -328,8 +340,8 @@ export component MenuBody(
328
340
  const document = body.ownerDocument;
329
341
  const trigger = triggerRef.current;
330
342
 
331
- const wanted = pendingFocus.current;
332
- pendingFocus.current = null;
343
+ const wanted = pendingFocusRef.current;
344
+ pendingFocusRef.current = null;
333
345
  const items = itemsOf(body, ITEM_SELECTOR, MENU_SELECTOR);
334
346
  const landing = moveTo(items, -1, wanted === "last" ? "last" : "first", false);
335
347
  // The menu itself when it holds nothing focusable, so focus is inside it
@@ -352,32 +364,47 @@ export component MenuBody(
352
364
  trigger?.focus?.();
353
365
  }
354
366
  };
355
- }, [menu.open, triggerRef, pendingFocus]);
367
+ // The ref objects are stable; this effect reads them after the opening commit.
368
+ // uf-lint-disable-next-line react-compiler/refs
369
+ }, [menu.open, triggerRef, pendingFocusRef]);
356
370
 
357
371
  // Only the outermost menu listens. A submenu closes with the tree, and two
358
372
  // of these would each answer the same press.
373
+ // The outside-interaction hook accepts refs; it reads them from event handlers.
374
+ // uf-lint-disable-next-line react-compiler/refs
359
375
  useInteractOutside({
376
+ // `open` is menu-tree metadata; the hook reads refs from event handlers.
377
+ // uf-lint-disable-next-line react-compiler/refs
360
378
  isDisabled: !menu.open || !isRoot,
361
379
  onInteractOutside: () => {
362
380
  dismissed.current = true;
363
381
  closeAll();
364
382
  },
383
+ // uf-lint-disable-next-line react-compiler/refs
365
384
  refs: [bodyRef, triggerRef],
366
385
  });
367
386
 
368
387
  const list = useMemo(() => ({ activeId, setActiveId }), [activeId]);
369
388
 
389
+ // `open` is menu-tree metadata; no ref value is read during render.
390
+ // uf-lint-disable-next-line react-compiler/refs
370
391
  if (!menu.open) {
371
392
  return null;
372
393
  }
373
394
 
374
395
  const props = withProps(withoutComposed(rest, ["onKeyDown", "ref"]), {
396
+ // `triggered` and `base` are menu-tree metadata, not ref values.
397
+ // uf-lint-disable-next-line react-compiler/refs
375
398
  "aria-labelledby": menu.triggered ? `${menu.base}-trigger` : undefined,
376
399
  "aria-orientation": "vertical",
377
400
  children,
378
401
  "data-align": anchored.align,
379
402
  "data-side": anchored.side,
403
+ // `base` is menu-tree metadata, not a ref value.
404
+ // uf-lint-disable-next-line react-compiler/refs
380
405
  id: `${menu.base}-body`,
406
+ // Key handling moves real DOM focus and records the active item after events.
407
+ // uf-lint-disable-next-line react-compiler/refs
381
408
  onKeyDown: composeHandlers(rest.onKeyDown, (event: PartEvent) => {
382
409
  const body: $FlowFixMe = event.currentTarget;
383
410
  const items = itemsOf(body, ITEM_SELECTOR, MENU_SELECTOR);
@@ -438,6 +465,8 @@ export component MenuBody(
438
465
  }
439
466
  }
440
467
  }),
468
+ // React calls callback refs during commit; placement and keyboard effects read it later.
469
+ // uf-lint-disable-next-line react-compiler/refs
441
470
  ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
442
471
  bodyRef.current = element;
443
472
  }),
@@ -709,7 +738,9 @@ export component MenuSubTrigger(children: React.Node, render?: RenderProp, ...re
709
738
  const id = `${menu.base}-trigger`;
710
739
 
711
740
  const open = () => {
712
- menu.pendingFocus.current = "first";
741
+ // This is an instruction for the submenu body after the opening commit.
742
+ // uf-lint-disable-next-line react-compiler/immutability
743
+ menu.pendingFocusRef.current = "first";
713
744
  menu.setOpen(true);
714
745
  };
715
746
 
@@ -733,6 +764,8 @@ export component MenuSubTrigger(children: React.Node, render?: RenderProp, ...re
733
764
  open();
734
765
  }),
735
766
  ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
767
+ // React calls callback refs during commit; the submenu body reads the trigger later.
768
+ // uf-lint-disable-next-line react-compiler/immutability
736
769
  menu.triggerRef.current = element;
737
770
  }),
738
771
  role: "menuitem",
package/menubar.js CHANGED
@@ -187,6 +187,8 @@ export component MenubarRoot(children: renders* MenubarMenu, render?: RenderProp
187
187
  setActive(value);
188
188
  }
189
189
  }),
190
+ // React calls callback refs during commit; keyboard handlers read it later.
191
+ // uf-lint-disable-next-line react-compiler/refs
190
192
  ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
191
193
  barRef.current = element;
192
194
  }),
@@ -271,10 +273,15 @@ export component MenubarTrigger(children: React.Node, render?: RenderProp, ...re
271
273
  return;
272
274
  }
273
275
  event.preventDefault();
274
- menu.pendingFocus.current = end;
276
+ // This is an instruction for the menu body after the opening commit.
277
+ // uf-lint-disable-next-line react-compiler/immutability
278
+ menu.pendingFocusRef.current = end;
275
279
  bar.setOpen(value);
276
280
  }),
281
+ // React calls callback refs during commit; menu focus restoration reads it later.
282
+ // uf-lint-disable-next-line react-compiler/refs
277
283
  ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
284
+ // uf-lint-disable-next-line react-compiler/immutability
278
285
  menu.triggerRef.current = element;
279
286
  }),
280
287
  role: "menuitem",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/ui",
3
- "version": "0.0.0-alpha.40",
3
+ "version": "0.0.0-alpha.42",
4
4
  "description": "Headless, accessible React components whose composition Flow checks, part of the Unified Toolchain for Flow.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,10 +19,10 @@
19
19
  "!*.test.js"
20
20
  ],
21
21
  "dependencies": {
22
- "@uniflowed/core": "0.0.0-alpha.40",
23
- "@uniflowed/hooks": "0.0.0-alpha.40",
24
- "@uniflowed/react": "0.0.0-alpha.40",
25
- "@uniflowed/state": "0.0.0-alpha.40"
22
+ "@uniflowed/core": "0.0.0-alpha.42",
23
+ "@uniflowed/hooks": "0.0.0-alpha.42",
24
+ "@uniflowed/react": "0.0.0-alpha.42",
25
+ "@uniflowed/state": "0.0.0-alpha.42"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "react": ">=19"
package/popover.js CHANGED
@@ -167,6 +167,8 @@ export component PopoverTrigger(children: React.Node, render?: RenderProp, ...re
167
167
  id: `${popover.base}-trigger`,
168
168
  onClick: composeHandlers(rest.onClick, () => popover.setOpen(!popover.open)),
169
169
  ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
170
+ // React calls callback refs during commit; focus restoration reads it later.
171
+ // uf-lint-disable-next-line react-compiler/immutability
170
172
  popover.triggerRef.current = element;
171
173
  }),
172
174
  });
@@ -220,13 +222,19 @@ export component PopoverBody(
220
222
  const left = useRef(false);
221
223
  const triggerRef = popover.triggerRef;
222
224
 
225
+ // useAnchor accepts ref objects and reads them from layout/effects.
226
+ // uf-lint-disable-next-line react-compiler/refs
223
227
  const anchored = useAnchor({
224
228
  align,
225
229
  alignOffset,
230
+ // uf-lint-disable-next-line react-compiler/refs
226
231
  anchorRef: triggerRef,
227
232
  avoidCollisions,
228
233
  collisionPadding,
234
+ // `open` is popover metadata; no ref value is read during render.
235
+ // uf-lint-disable-next-line react-compiler/refs
229
236
  open: popover.open,
237
+ // uf-lint-disable-next-line react-compiler/refs
230
238
  overlayRef: bodyRef,
231
239
  side,
232
240
  sideOffset,
@@ -286,19 +294,28 @@ export component PopoverBody(
286
294
  opener?.focus?.();
287
295
  }
288
296
  };
297
+ // The ref objects are stable; this effect reads them after the opening commit.
298
+ // uf-lint-disable-next-line react-compiler/refs
289
299
  }, [popover.open, triggerRef, close, initialFocus]);
290
300
 
291
301
  // A press outside is the other way a reader leaves, and the trigger is not
292
302
  // "outside" for the reason the module header gives.
303
+ // The outside-interaction hook accepts refs; it reads them from event handlers.
304
+ // uf-lint-disable-next-line react-compiler/refs
293
305
  useInteractOutside({
306
+ // `open` is popover metadata; the hook reads refs from event handlers.
307
+ // uf-lint-disable-next-line react-compiler/refs
294
308
  isDisabled: !popover.open,
295
309
  onInteractOutside: () => {
296
310
  left.current = true;
297
311
  close();
298
312
  },
313
+ // uf-lint-disable-next-line react-compiler/refs
299
314
  refs: [bodyRef, triggerRef],
300
315
  });
301
316
 
317
+ // `open` is popover metadata; no ref value is read during render.
318
+ // uf-lint-disable-next-line react-compiler/refs
302
319
  if (!popover.open) {
303
320
  return null;
304
321
  }
@@ -310,11 +327,15 @@ export component PopoverBody(
310
327
  const named = rest["aria-label"] != null || rest["aria-labelledby"] != null;
311
328
 
312
329
  const props = withProps(passed, {
330
+ // `triggered` and `base` are popover metadata, not ref values.
331
+ // uf-lint-disable-next-line react-compiler/refs
313
332
  "aria-labelledby": named || !popover.triggered ? undefined : `${popover.base}-trigger`,
314
333
  children,
315
334
  "data-align": anchored.align,
316
335
  "data-side": anchored.side,
317
336
  "data-state": "open",
337
+ // `base` is popover metadata, not a ref value.
338
+ // uf-lint-disable-next-line react-compiler/refs
318
339
  id: `${popover.base}-body`,
319
340
  onKeyDown: composeHandlers(rest.onKeyDown, (event: PartEvent) => {
320
341
  if (event.key !== "Escape") {
@@ -326,6 +347,8 @@ export component PopoverBody(
326
347
  event.stopPropagation();
327
348
  close();
328
349
  }),
350
+ // React calls callback refs during commit; placement effects and traps read it later.
351
+ // uf-lint-disable-next-line react-compiler/refs
329
352
  ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
330
353
  bodyRef.current = element;
331
354
  }),
package/radio-group.js CHANGED
@@ -208,6 +208,8 @@ export component RadioGroupRoot(
208
208
  select(next.getAttribute("data-value") ?? "");
209
209
  }
210
210
  }),
211
+ // React calls callback refs during commit; keyboard handlers read it later.
212
+ // uf-lint-disable-next-line react-compiler/refs
211
213
  ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
212
214
  rootRef.current = element;
213
215
  }),
package/resizable.js CHANGED
@@ -332,6 +332,8 @@ export component ResizableHandle(label?: string = "Resize", ...rest: Rest) {
332
332
  aria-valuemax={group.max}
333
333
  aria-valuemin={group.min}
334
334
  aria-valuenow={group.value}
335
+ // Key and pointer handlers keep gesture state in refs between events.
336
+ // uf-lint-disable-next-line react-compiler/refs
335
337
  onKeyDown={composeHandlers(rest.onKeyDown, (event: $FlowFixMe) => {
336
338
  if (group.disabled) {
337
339
  return;
@@ -373,7 +375,9 @@ export component ResizableHandle(label?: string = "Resize", ...rest: Rest) {
373
375
  moveBy(amount);
374
376
  }
375
377
  })}
378
+ // uf-lint-disable-next-line react-compiler/refs
376
379
  onPointerCancel={composeHandlers(rest.onPointerCancel, endDrag)}
380
+ // uf-lint-disable-next-line react-compiler/refs
377
381
  onPointerDown={composeHandlers(rest.onPointerDown, (event: $FlowFixMe) => {
378
382
  if (group.disabled) {
379
383
  return;
@@ -395,6 +399,7 @@ export component ResizableHandle(label?: string = "Resize", ...rest: Rest) {
395
399
  // it to move. The module header says what a press does on a track
396
400
  // instead, and why the two are not the same gesture.
397
401
  })}
402
+ // uf-lint-disable-next-line react-compiler/refs
398
403
  onPointerMove={composeHandlers(rest.onPointerMove, (event: $FlowFixMe) => {
399
404
  if (!dragging.current) {
400
405
  return;
@@ -404,6 +409,7 @@ export component ResizableHandle(label?: string = "Resize", ...rest: Rest) {
404
409
  group.setValue(share);
405
410
  }
406
411
  })}
412
+ // uf-lint-disable-next-line react-compiler/refs
407
413
  onPointerUp={composeHandlers(rest.onPointerUp, endDrag)}
408
414
  role="separator"
409
415
  // A separator that is not in the tab sequence is the WCAG 2.1.1 failure
package/scroll-area.js CHANGED
@@ -72,10 +72,10 @@ type ScrollAreaState = {|
72
72
  readonly base: string,
73
73
  readonly label: string,
74
74
  readonly viewportRef: { current: HTMLElement | null },
75
- readonly remembered: { current: Offset },
75
+ readonly rememberedRef: { current: Offset },
76
76
  /** Written by the viewport, read by every scrollbar. */
77
77
  readonly report: () => void,
78
- readonly scrollbars: { current: Array<HTMLElement> },
78
+ readonly scrollbarsRef: { current: Array<HTMLElement> },
79
79
  |};
80
80
 
81
81
  const ScrollAreaContext: React.Context<ScrollAreaState | null> = createContext(null);
@@ -106,24 +106,24 @@ hook useScrollArea(part: string): ScrollAreaState {
106
106
  export component ScrollAreaRoot(children: React.Node, label: string, ...rest: Rest) {
107
107
  const base = useId();
108
108
  const viewportRef = useRef<HTMLElement | null>(null);
109
- const remembered = useRef<Offset>({ x: 0, y: 0 });
110
- const scrollbars = useRef<Array<HTMLElement>>([]);
109
+ const rememberedRef = useRef<Offset>({ x: 0, y: 0 });
110
+ const scrollbarsRef = useRef<Array<HTMLElement>>([]);
111
111
 
112
112
  const state = useMemo(
113
113
  () => ({
114
114
  base,
115
115
  label,
116
- remembered,
116
+ rememberedRef,
117
117
  report: () => {
118
118
  const viewport = viewportRef.current;
119
119
  if (viewport == null) {
120
120
  return;
121
121
  }
122
- for (const scrollbar of scrollbars.current) {
122
+ for (const scrollbar of scrollbarsRef.current) {
123
123
  write(scrollbar, viewport);
124
124
  }
125
125
  },
126
- scrollbars,
126
+ scrollbarsRef,
127
127
  viewportRef,
128
128
  }),
129
129
  [base, label],
@@ -145,7 +145,7 @@ export component ScrollAreaRoot(children: React.Node, label: string, ...rest: Re
145
145
  */
146
146
  export component ScrollAreaViewport(children: React.Node, ...rest: Rest) {
147
147
  const area = useScrollArea("ScrollArea.Viewport");
148
- const { remembered, report, viewportRef } = area;
148
+ const { rememberedRef, report, viewportRef } = area;
149
149
  const passed = withoutComposed(rest, ["ref"]);
150
150
 
151
151
  useEventListener(viewportRef, "scroll", () => {
@@ -155,7 +155,7 @@ export component ScrollAreaViewport(children: React.Node, ...rest: Rest) {
155
155
  }
156
156
  // The reader's own position, including a deliberate scroll back to the
157
157
  // top — which is why the restore below never fights them.
158
- remembered.current = { x: viewport.scrollLeft, y: viewport.scrollTop };
158
+ rememberedRef.current = { x: viewport.scrollLeft, y: viewport.scrollTop };
159
159
  report();
160
160
  });
161
161
 
@@ -167,7 +167,7 @@ export component ScrollAreaViewport(children: React.Node, ...rest: Rest) {
167
167
  if (viewport == null) {
168
168
  return;
169
169
  }
170
- const { x, y } = remembered.current;
170
+ const { x, y } = rememberedRef.current;
171
171
  if (y !== 0 && viewport.scrollTop === 0) {
172
172
  viewport.scrollTop = y;
173
173
  }
@@ -213,7 +213,7 @@ export component ScrollAreaScrollbar(
213
213
  ...rest: Rest
214
214
  ) {
215
215
  const area = useScrollArea("ScrollArea.Scrollbar");
216
- const { report, scrollbars } = area;
216
+ const { report, scrollbarsRef } = area;
217
217
  const passed = withoutComposed(rest, ["ref"]);
218
218
 
219
219
  return (
@@ -225,8 +225,8 @@ export component ScrollAreaScrollbar(
225
225
  aria-hidden="true"
226
226
  data-orientation={orientation}
227
227
  ref={composeRefs(rest.ref, (element: HTMLElement | null) => {
228
- const kept = scrollbars.current.filter((each) => each !== element);
229
- scrollbars.current = element == null ? kept : [...kept, element];
228
+ const kept = scrollbarsRef.current.filter((each) => each !== element);
229
+ scrollbarsRef.current = element == null ? kept : [...kept, element];
230
230
  report();
231
231
  })}
232
232
  >
package/select.js CHANGED
@@ -192,7 +192,7 @@ type SelectState = {|
192
192
  /** The id of the option `aria-activedescendant` names, if any. */
193
193
  readonly activeId: string | null,
194
194
  readonly setActiveId: (id: string | null) => void,
195
- readonly pendingLanding: { current: Landing | null },
195
+ readonly pendingLandingRef: { current: Landing | null },
196
196
  readonly triggerRef: { current: HTMLElement | null },
197
197
  readonly listRef: { current: HTMLElement | null },
198
198
  /**
@@ -270,7 +270,7 @@ export component SelectRoot(
270
270
  const [activeId, setActiveId] = useState<string | null>(null);
271
271
  const [labels, setLabels] = useState<{ readonly [string]: string }>({});
272
272
  const [labelled, setLabelled] = useState(false);
273
- const pendingLanding = useRef<Landing | null>(null);
273
+ const pendingLandingRef = useRef<Landing | null>(null);
274
274
  const triggerRef = useRef<HTMLElement | null>(null);
275
275
  const listRef = useRef<HTMLElement | null>(null);
276
276
  const typeahead = useTypeahead();
@@ -313,7 +313,7 @@ export component SelectRoot(
313
313
  choose,
314
314
  activeId,
315
315
  setActiveId,
316
- pendingLanding,
316
+ pendingLandingRef,
317
317
  triggerRef,
318
318
  listRef,
319
319
  labels,
@@ -415,7 +415,9 @@ export component SelectTrigger(children: React.Node, ...rest: Rest) {
415
415
  /** Move the cursor within an open list, or open with an instruction. */
416
416
  const move = (end: Movement, preferSelected: boolean) => {
417
417
  if (!select.open) {
418
- select.pendingLanding.current = { kind: "end", end, preferSelected };
418
+ // This is an instruction for the list after the opening commit.
419
+ // uf-lint-disable-next-line react-compiler/immutability
420
+ select.pendingLandingRef.current = { kind: "end", end, preferSelected };
419
421
  select.setOpen(true);
420
422
  return;
421
423
  }
@@ -458,7 +460,9 @@ export component SelectTrigger(children: React.Node, ...rest: Rest) {
458
460
  select.setActiveId(null);
459
461
  return;
460
462
  }
461
- select.pendingLanding.current = { kind: "end", end: "first", preferSelected: true };
463
+ // This is an instruction for the list after the opening commit.
464
+ // uf-lint-disable-next-line react-compiler/immutability
465
+ select.pendingLandingRef.current = { kind: "end", end: "first", preferSelected: true };
462
466
  select.setOpen(true);
463
467
  })}
464
468
  onKeyDown={composeHandlers(rest.onKeyDown, (event: $FlowFixMe) => {
@@ -500,7 +504,9 @@ export component SelectTrigger(children: React.Node, ...rest: Rest) {
500
504
  // again as a click.
501
505
  event.preventDefault();
502
506
  if (!select.open) {
503
- select.pendingLanding.current = { kind: "end", end: "first", preferSelected: true };
507
+ // This is an instruction for the list after the opening commit.
508
+ // uf-lint-disable-next-line react-compiler/immutability
509
+ select.pendingLandingRef.current = { kind: "end", end: "first", preferSelected: true };
504
510
  select.setOpen(true);
505
511
  return;
506
512
  }
@@ -544,7 +550,8 @@ export component SelectTrigger(children: React.Node, ...rest: Rest) {
544
550
  event.preventDefault();
545
551
  // The options are not in the document yet, so the keystroke travels
546
552
  // to the commit that renders them.
547
- select.pendingLanding.current = { kind: "typed", key: event.key };
553
+ // uf-lint-disable-next-line react-compiler/immutability
554
+ select.pendingLandingRef.current = { kind: "typed", key: event.key };
548
555
  select.setOpen(true);
549
556
  return;
550
557
  }
@@ -560,6 +567,8 @@ export component SelectTrigger(children: React.Node, ...rest: Rest) {
560
567
  }
561
568
  })}
562
569
  ref={composeRefs(rest.ref, (element) => {
570
+ // React calls callback refs during commit; the list reads the trigger later.
571
+ // uf-lint-disable-next-line react-compiler/immutability
563
572
  select.triggerRef.current = element;
564
573
  })}
565
574
  role="combobox"
@@ -628,7 +637,8 @@ export component SelectList(
628
637
  ...rest: Rest
629
638
  ) {
630
639
  const select = useSelect("Select.List");
631
- const { activeId, listRef, pendingLanding, setActiveId, triggerRef, typeahead, value } = select;
640
+ const { activeId, listRef, pendingLandingRef, setActiveId, triggerRef, typeahead, value } =
641
+ select;
632
642
  const close = useStableCallback(() => {
633
643
  select.setOpen(false);
634
644
  select.setActiveId(null);
@@ -656,6 +666,8 @@ export component SelectList(
656
666
  // render — a change to `children` that no dependency list can describe. Every
657
667
  // write is guarded by a comparison, so it settles after one extra pass rather
658
668
  // than looping.
669
+ // This effect measures caller-rendered options after commit.
670
+ // uf-lint-disable-next-line react-compiler/immutability
659
671
  useEffect(() => {
660
672
  const list = listRef.current;
661
673
  if (list == null) {
@@ -663,9 +675,10 @@ export component SelectList(
663
675
  }
664
676
  const items = itemsOf(list, OPTION_SELECTOR, LISTBOX_SELECTOR);
665
677
 
666
- const wanted = pendingLanding.current;
678
+ const wanted = pendingLandingRef.current;
667
679
  if (wanted != null) {
668
- pendingLanding.current = null;
680
+ // uf-lint-disable-next-line react-compiler/immutability
681
+ pendingLandingRef.current = null;
669
682
  // An `if` rather than a `match` on `wanted.kind`, because matching on a
670
683
  // property does not refine the object that property came from: inside
671
684
  // `match (wanted.kind)` both arms still see the whole union, and `uf
package/sidebar.js CHANGED
@@ -275,7 +275,12 @@ export component SidebarItem(
275
275
  ...rest: Rest
276
276
  ) {
277
277
  const sidebar = useSidebar("Sidebar.Item");
278
+ // This removes the caller's ref from rest props; it does not read a ref value.
279
+ // uf-lint-disable-next-line react-compiler/refs
278
280
  const passed = withoutComposed(rest, ["ref"]);
281
+ // The forwarded ref is passed through to whichever entry wrapper renders.
282
+ // uf-lint-disable-next-line react-compiler/refs
283
+ const forwardedRef = rest.ref;
279
284
  const mine: Rest = {
280
285
  "aria-label": sidebar.collapsed ? label : undefined,
281
286
  children,
@@ -293,7 +298,9 @@ export component SidebarItem(
293
298
  };
294
299
 
295
300
  if (!sidebar.collapsed) {
296
- return entry({ ref: rest.ref });
301
+ // The forwarded ref is passed through to the rendered entry.
302
+ // uf-lint-disable-next-line react-compiler/refs
303
+ return entry({ ref: forwardedRef });
297
304
  }
298
305
 
299
306
  // The icon's name, shown. A reader who can see the rail and not read minds
@@ -301,7 +308,7 @@ export component SidebarItem(
301
308
  // the mechanism that already satisfies WCAG 1.4.13 in this package.
302
309
  return (
303
310
  <TooltipRoot>
304
- <TooltipTrigger ref={rest.ref} render={(props: Rest) => entry(props)} />
311
+ <TooltipTrigger ref={forwardedRef} render={(props: Rest) => entry(props)} />
305
312
  <TooltipBody
306
313
  {...forwarded(tooltipProps ?? {})}
307
314
  side={sidebar.side === "left" ? "right" : "left"}
package/skeleton.js CHANGED
@@ -108,9 +108,13 @@ export component SkeletonRoot(
108
108
  useEffect(() => {
109
109
  if (busy) {
110
110
  waited.current = true;
111
+ // The live-region text changes after commit so assistive tech can announce it.
112
+ // uf-lint-disable-next-line react-compiler/set-state-in-effect
111
113
  setMessage(label);
112
114
  return;
113
115
  }
116
+ // The live-region text changes after commit so assistive tech can announce it.
117
+ // uf-lint-disable-next-line react-compiler/set-state-in-effect
114
118
  setMessage(waited.current ? doneLabel : "");
115
119
  }, [busy, doneLabel, label]);
116
120
 
package/slider.js CHANGED
@@ -247,6 +247,8 @@ export component SliderTrack(children: React.Node, render?: RenderProp, ...rest:
247
247
 
248
248
  const props = withProps(passed, {
249
249
  children,
250
+ // Pointer handlers keep the active thumb in a ref between events.
251
+ // uf-lint-disable-next-line react-compiler/refs
250
252
  onPointerDown: composeHandlers(rest.onPointerDown, (event: $FlowFixMe) => {
251
253
  if (slider.disabled) {
252
254
  return;
@@ -257,16 +259,20 @@ export component SliderTrack(children: React.Node, render?: RenderProp, ...rest:
257
259
  event.currentTarget?.setPointerCapture?.(event.pointerId);
258
260
  moveTo(event, null);
259
261
  }),
262
+ // uf-lint-disable-next-line react-compiler/refs
260
263
  onPointerMove: composeHandlers(rest.onPointerMove, (event: $FlowFixMe) => {
261
264
  if (dragging.current != null) {
262
265
  moveTo(event, dragging.current);
263
266
  }
264
267
  }),
268
+ // uf-lint-disable-next-line react-compiler/refs
265
269
  onPointerUp: composeHandlers(rest.onPointerUp, (event: $FlowFixMe) => {
266
270
  dragging.current = null;
267
271
  event.currentTarget?.releasePointerCapture?.(event.pointerId);
268
272
  }),
269
273
  ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
274
+ // React calls callback refs during commit; pointer handlers read the track later.
275
+ // uf-lint-disable-next-line react-compiler/immutability
270
276
  slider.trackRef.current = element;
271
277
  }),
272
278
  });