@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.
package/accordion.js CHANGED
@@ -347,6 +347,8 @@ export component AccordionContent(children: React.Node, render?: RenderProp, ...
347
347
  children,
348
348
  hidden: !item.open,
349
349
  id: item.contentId,
350
+ // React calls callback refs during commit; this node is only read by effects.
351
+ // uf-lint-disable-next-line react-compiler/refs
350
352
  ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
351
353
  contentRef.current = element;
352
354
  }),
package/alert-dialog.js CHANGED
@@ -227,6 +227,8 @@ export component AlertDialogDescription(children: React.Node, render?: RenderPro
227
227
  const describedBy = alert.describedBy;
228
228
 
229
229
  useEffect(() => {
230
+ // This mount counter is a ref because it is read by Dialog.Body after commit.
231
+ // uf-lint-disable-next-line react-compiler/immutability
230
232
  describedBy.current += 1;
231
233
  return () => {
232
234
  describedBy.current -= 1;
package/avatar.js CHANGED
@@ -223,6 +223,8 @@ export component AvatarImage(
223
223
  alt,
224
224
  onError: composeHandlers(rest.onError, () => report(source, "error")),
225
225
  onLoad: composeHandlers(rest.onLoad, () => report(source, "loaded")),
226
+ // React calls callback refs during commit; the load effect reads it later.
227
+ // uf-lint-disable-next-line react-compiler/refs
226
228
  ref: composeRefs(rest.ref, (node: HTMLImageElement | null) => {
227
229
  element.current = node;
228
230
  }),
@@ -261,6 +263,8 @@ export component AvatarFallback(
261
263
 
262
264
  useEffect(() => {
263
265
  if (!waiting) {
266
+ // The fallback delay resets after the image leaves its loading window.
267
+ // uf-lint-disable-next-line react-compiler/set-state-in-effect
264
268
  setElapsed(false);
265
269
  }
266
270
  }, [waiting]);
package/calendar.js CHANGED
@@ -138,7 +138,7 @@ type CalendarState = {|
138
138
  * November — a cell that is rendered for the first time by the same update
139
139
  * that asked for it, so nothing could have focused it when the key arrived.
140
140
  */
141
- readonly pendingFocus: { current: string | null },
141
+ readonly pendingFocusRef: { current: string | null },
142
142
  readonly select: (date: PlainDate) => void,
143
143
  readonly selected: PlainDate | null,
144
144
  readonly today: PlainDate,
@@ -190,7 +190,7 @@ export component CalendarRoot(
190
190
  ...rest: Rest
191
191
  ) {
192
192
  const base = useId();
193
- const pendingFocus = useRef<string | null>(null);
193
+ const pendingFocusRef = useRef<string | null>(null);
194
194
  // One is always allocated, because a hook may not be called conditionally;
195
195
  // the caller's is used when there is one.
196
196
  const ownDayRef = useRef<HTMLElement | null>(null);
@@ -238,7 +238,7 @@ export component CalendarRoot(
238
238
  if (viaKeyboard) {
239
239
  // Only for the keyboard. A press already put focus on the cell it landed
240
240
  // on, and asking for it again would fight a caller who moved it.
241
- pendingFocus.current = date.toString();
241
+ pendingFocusRef.current = date.toString();
242
242
  }
243
243
  // Compared rather than assigned, because `Calendar.Day` reports the focus
244
244
  // that this very call produced: a key moves the tab stop, the effect focuses
@@ -286,7 +286,7 @@ export component CalendarRoot(
286
286
  isDisabled,
287
287
  locale,
288
288
  moveFocus,
289
- pendingFocus,
289
+ pendingFocusRef,
290
290
  select,
291
291
  selected,
292
292
  today: currentDate,
@@ -336,7 +336,7 @@ export component CalendarRoot(
336
336
  export component CalendarMonth(children?: (date: PlainDate) => renders CalendarDay, ...rest: Rest) {
337
337
  const calendar = useCalendar("Calendar.Month");
338
338
  const gridRef = useRef<HTMLElement | null>(null);
339
- const { focused, moveFocus, pendingFocus, weekStartsOn } = calendar;
339
+ const { focused, focusedDayRef, moveFocus, pendingFocusRef, weekStartsOn } = calendar;
340
340
 
341
341
  const weeks = useMemo(
342
342
  () => weeksOf(focused.year, focused.month, weekStartsOn),
@@ -355,15 +355,15 @@ export component CalendarMonth(children?: (date: PlainDate) => renders CalendarD
355
355
  }
356
356
  const at = focused.toString();
357
357
  const cell: HTMLElement | null = (grid.querySelector(`[data-date="${at}"]`): $FlowFixMe);
358
- calendar.focusedDayRef.current = cell;
358
+ focusedDayRef.current = cell;
359
359
  // Only when a key asked for it, and only once the render it asked for has
360
360
  // happened: `ArrowRight` off the end of October sets this to the 1st of
361
361
  // November, and the cell exists for the first time in the commit this
362
362
  // effect belongs to.
363
- if (pendingFocus.current !== at) {
363
+ if (pendingFocusRef.current !== at) {
364
364
  return;
365
365
  }
366
- pendingFocus.current = null;
366
+ pendingFocusRef.current = null;
367
367
  cell?.focus();
368
368
  });
369
369
 
package/collapsible.js CHANGED
@@ -157,6 +157,8 @@ export component CollapsibleContent(children: React.Node, render?: RenderProp, .
157
157
  children,
158
158
  hidden: !collapsible.open,
159
159
  id: collapsible.contentId,
160
+ // React calls callback refs during commit; this node is only read by effects.
161
+ // uf-lint-disable-next-line react-compiler/refs
160
162
  ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
161
163
  contentRef.current = element;
162
164
  }),
package/combobox.js CHANGED
@@ -165,7 +165,7 @@ type ComboboxState = {|
165
165
  * and the list does not exist to be measured until the next commit. A ref
166
166
  * rather than state because nothing renders it.
167
167
  */
168
- readonly pendingActive: { current: "first" | "last" | null },
168
+ readonly pendingActiveRef: { current: "first" | "last" | null },
169
169
  readonly inputRef: { current: HTMLElement | null },
170
170
  readonly listRef: { current: HTMLElement | null },
171
171
  /** How many options are in the list, for the live region. */
@@ -231,7 +231,7 @@ export component ComboboxRoot(
231
231
  const [activeId, setActiveId] = useState<string | null>(null);
232
232
  const [count, setCount] = useState(0);
233
233
  const [labelled, setLabelled] = useState(false);
234
- const pendingActive = useRef<"first" | "last" | null>(null);
234
+ const pendingActiveRef = useRef<"first" | "last" | null>(null);
235
235
  const inputRef = useRef<HTMLElement | null>(null);
236
236
  const listRef = useRef<HTMLElement | null>(null);
237
237
 
@@ -265,7 +265,7 @@ export component ComboboxRoot(
265
265
  clear,
266
266
  activeId,
267
267
  setActiveId,
268
- pendingActive,
268
+ pendingActiveRef,
269
269
  inputRef,
270
270
  listRef,
271
271
  count,
@@ -312,6 +312,8 @@ export component ComboboxLabel(children: React.Node, ...rest: Rest) {
312
312
  /** The text field, and every key the pattern defines. */
313
313
  export component ComboboxInput(...rest: Rest) {
314
314
  const combobox = useCombobox("Combobox.Input");
315
+ // `rest` filtering is render-time props work; ref objects are only passed through later.
316
+ // uf-lint-disable-next-line react-compiler/refs
315
317
  const passed = withoutComposed(rest, ["onChange", "onKeyDown", "ref"]);
316
318
 
317
319
  /** The options in the document right now, in document order. */
@@ -325,7 +327,8 @@ export component ComboboxInput(...rest: Rest) {
325
327
  if (items.length === 0) {
326
328
  // The list is not in the document yet, so leave an instruction for the
327
329
  // commit that puts it there.
328
- combobox.pendingActive.current = movement === "next" ? "first" : "last";
330
+ // uf-lint-disable-next-line react-compiler/immutability
331
+ combobox.pendingActiveRef.current = movement === "next" ? "first" : "last";
329
332
  return;
330
333
  }
331
334
  const at = items.findIndex((item) => item.id === combobox.activeId);
@@ -358,6 +361,8 @@ export component ComboboxInput(...rest: Rest) {
358
361
  // The browser's own dropdown would sit on top of this one.
359
362
  autoComplete="off"
360
363
  id={`${combobox.base}-input`}
364
+ // Input events read list refs and update the virtual active descendant.
365
+ // uf-lint-disable-next-line react-compiler/refs
361
366
  onChange={composeHandlers(rest.onChange, (event: $FlowFixMe) => {
362
367
  combobox.setText(event.target.value);
363
368
  combobox.setOpen(true);
@@ -366,6 +371,8 @@ export component ComboboxInput(...rest: Rest) {
366
371
  // Enter takes something the reader can no longer see.
367
372
  combobox.setActiveId(null);
368
373
  })}
374
+ // Key events read list refs and update the virtual active descendant.
375
+ // uf-lint-disable-next-line react-compiler/refs
369
376
  onKeyDown={composeHandlers(rest.onKeyDown, (event) => {
370
377
  if (event.key === "ArrowDown" || event.key === "ArrowUp") {
371
378
  event.preventDefault();
@@ -411,7 +418,10 @@ export component ComboboxInput(...rest: Rest) {
411
418
  combobox.setActiveId(null);
412
419
  }
413
420
  })}
421
+ // React calls callback refs during commit; keyboard handlers read the input later.
422
+ // uf-lint-disable-next-line react-compiler/refs
414
423
  ref={composeRefs(rest.ref, (element) => {
424
+ // uf-lint-disable-next-line react-compiler/immutability
415
425
  combobox.inputRef.current = element;
416
426
  })}
417
427
  role="combobox"
@@ -443,7 +453,7 @@ export component ComboboxList(
443
453
  ...rest: Rest
444
454
  ) {
445
455
  const combobox = useCombobox("Combobox.List");
446
- const { activeId, count, listRef, inputRef, pendingActive, setActiveId, setCount } = combobox;
456
+ const { activeId, count, listRef, inputRef, pendingActiveRef, setActiveId, setCount } = combobox;
447
457
  const close = useStableCallback(() => {
448
458
  combobox.setOpen(false);
449
459
  combobox.setActiveId(null);
@@ -453,13 +463,19 @@ export component ComboboxList(
453
463
  // `align="start"` because a list of options belongs under the edge the text
454
464
  // starts at, and `--uf-anchor-trigger-width` is what a stylesheet reads to
455
465
  // make it exactly as wide as the field.
466
+ // useAnchor accepts ref objects and reads them from layout/effects.
467
+ // uf-lint-disable-next-line react-compiler/refs
456
468
  const anchored = useAnchor({
457
469
  align,
458
470
  alignOffset,
471
+ // uf-lint-disable-next-line react-compiler/refs
459
472
  anchorRef: inputRef,
460
473
  avoidCollisions,
461
474
  collisionPadding,
475
+ // `open` is combobox metadata; no ref value is read during render.
476
+ // uf-lint-disable-next-line react-compiler/refs
462
477
  open: combobox.open,
478
+ // uf-lint-disable-next-line react-compiler/refs
463
479
  overlayRef: listRef,
464
480
  side,
465
481
  sideOffset,
@@ -470,6 +486,8 @@ export component ComboboxList(
470
486
  // `children` that no dependency list can describe. Every write below is
471
487
  // guarded by a comparison, so the effect settles after one extra pass rather
472
488
  // than looping.
489
+ // This effect measures caller-rendered options after commit.
490
+ // uf-lint-disable-next-line react-compiler/immutability
473
491
  useEffect(() => {
474
492
  const list = listRef.current;
475
493
  if (list == null) {
@@ -485,9 +503,10 @@ export component ComboboxList(
485
503
  setCount(items.length);
486
504
  }
487
505
 
488
- const wanted = pendingActive.current;
506
+ const wanted = pendingActiveRef.current;
489
507
  if (wanted != null) {
490
- pendingActive.current = null;
508
+ // uf-lint-disable-next-line react-compiler/immutability
509
+ pendingActiveRef.current = null;
491
510
  setActiveId(moveTo(items, -1, wanted, false)?.id ?? null);
492
511
  return;
493
512
  }
package/context-menu.js CHANGED
@@ -141,7 +141,9 @@ export component ContextMenuTrigger(children: React.Node, render?: RenderProp, .
141
141
  // No point: the menu goes against the element, which is where the reader's
142
142
  // focus already is.
143
143
  openAt(null);
144
- menu.pendingFocus.current = "first";
144
+ // This is an instruction for the menu body after the opening commit.
145
+ // uf-lint-disable-next-line react-compiler/immutability
146
+ menu.pendingFocusRef.current = "first";
145
147
  menu.setOpen(true);
146
148
  }, [menu, openAt]);
147
149
 
@@ -150,7 +152,9 @@ export component ContextMenuTrigger(children: React.Node, render?: RenderProp, .
150
152
  useLongPress(triggerRef, (event: Event) => {
151
153
  const pointer: $FlowFixMe = event;
152
154
  openAt(pointAt(pointer.clientX ?? 0, pointer.clientY ?? 0));
153
- menu.pendingFocus.current = "first";
155
+ // This is an instruction for the menu body after the opening commit.
156
+ // uf-lint-disable-next-line react-compiler/immutability
157
+ menu.pendingFocusRef.current = "first";
154
158
  menu.setOpen(true);
155
159
  });
156
160
 
@@ -173,7 +177,9 @@ export component ContextMenuTrigger(children: React.Node, render?: RenderProp, .
173
177
  // commands the page has for what they pressed on.
174
178
  press.preventDefault();
175
179
  openAt(pointAt(press.clientX ?? 0, press.clientY ?? 0));
176
- menu.pendingFocus.current = "first";
180
+ // This is an instruction for the menu body after the opening commit.
181
+ // uf-lint-disable-next-line react-compiler/immutability
182
+ menu.pendingFocusRef.current = "first";
177
183
  menu.setOpen(true);
178
184
  }),
179
185
  onKeyDown: composeHandlers(rest.onKeyDown, (event: PartEvent) => {
@@ -188,10 +194,13 @@ export component ContextMenuTrigger(children: React.Node, render?: RenderProp, .
188
194
  event.preventDefault();
189
195
  openHere();
190
196
  }),
197
+ // React calls callback refs during commit; focus restoration reads these later.
198
+ // uf-lint-disable-next-line react-compiler/refs
191
199
  ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
192
200
  triggerRef.current = element;
193
201
  // What focus goes back to when the menu closes. It is deliberately not
194
202
  // registered as the menu's *name*; see the module header.
203
+ // uf-lint-disable-next-line react-compiler/immutability
195
204
  menu.triggerRef.current = element;
196
205
  }),
197
206
  },
package/date-picker.js CHANGED
@@ -253,18 +253,26 @@ const CalendarSettings: React.Context<CalendarSettingsValue> = createContext({
253
253
  */
254
254
  export component DatePickerInput(...rest: Rest) {
255
255
  const picker = useDatePicker("DatePicker.Input");
256
+ // `rest` filtering is render-time props work; ref objects are only passed through later.
257
+ // uf-lint-disable-next-line react-compiler/refs
256
258
  const passed = withoutComposed(rest, ["onBlur", "onChange", "onKeyDown", "ref"]);
257
259
 
258
260
  return (
259
261
  <input
260
262
  {...passed}
261
263
  aria-invalid={picker.invalid ? "true" : undefined}
264
+ // Input events commit typed text while focus stays on the field ref.
265
+ // uf-lint-disable-next-line react-compiler/refs
262
266
  onBlur={composeHandlers(rest.onBlur, (event) => {
263
267
  picker.commit((event.currentTarget: $FlowFixMe).value);
264
268
  })}
269
+ // Input events commit typed text while focus stays on the field ref.
270
+ // uf-lint-disable-next-line react-compiler/refs
265
271
  onChange={composeHandlers(rest.onChange, (event) => {
266
272
  picker.setDraft((event.currentTarget: $FlowFixMe).value);
267
273
  })}
274
+ // Key events commit typed text while focus stays on the field ref.
275
+ // uf-lint-disable-next-line react-compiler/refs
268
276
  onKeyDown={composeHandlers(rest.onKeyDown, (event) => {
269
277
  if (event.key !== "Enter") {
270
278
  return;
@@ -274,7 +282,10 @@ export component DatePickerInput(...rest: Rest) {
274
282
  event.preventDefault();
275
283
  picker.commit((event.currentTarget: $FlowFixMe).value);
276
284
  })}
285
+ // React calls callback refs during commit; the calendar and picker read the field later.
286
+ // uf-lint-disable-next-line react-compiler/refs
277
287
  ref={composeRefs(rest.ref, (element) => {
288
+ // uf-lint-disable-next-line react-compiler/immutability
278
289
  picker.fieldRef.current = element;
279
290
  })}
280
291
  type="text"
package/dialog.js CHANGED
@@ -182,6 +182,8 @@ export component DialogTrigger(children: React.Node, render?: RenderProp, ...res
182
182
  children,
183
183
  onClick: composeHandlers(rest.onClick, () => dialog.setOpen(true)),
184
184
  ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
185
+ // React calls callback refs during commit; focus restoration reads it later.
186
+ // uf-lint-disable-next-line react-compiler/immutability
185
187
  dialog.triggerRef.current = element;
186
188
  }),
187
189
  });
@@ -315,6 +317,8 @@ export component DialogBody(
315
317
  "aria-modal": "true",
316
318
  children,
317
319
  id: `${dialog.base}-body`,
320
+ // Key handling closes the mounted dialog and restores focus after events.
321
+ // uf-lint-disable-next-line react-compiler/refs
318
322
  onKeyDown: composeHandlers(rest.onKeyDown, (event: PartEvent) => {
319
323
  if (event.key === "Escape") {
320
324
  event.preventDefault();
@@ -353,6 +357,8 @@ export component DialogBody(
353
357
  first.focus();
354
358
  }
355
359
  }),
360
+ // React calls callback refs during commit; focus effects and traps read it later.
361
+ // uf-lint-disable-next-line react-compiler/refs
356
362
  ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
357
363
  bodyRef.current = element;
358
364
  }),
package/drawer.js CHANGED
@@ -102,8 +102,8 @@ type DrawerState = {|
102
102
  * child's effect runs before its parent's, so `Drawer.Body` can ask about
103
103
  * both on the commit that mounted them, and nothing renders either number.
104
104
  */
105
- readonly closes: { current: number },
106
- readonly handles: { current: number },
105
+ readonly closeCountRef: { current: number },
106
+ readonly handleCountRef: { current: number },
107
107
  |};
108
108
 
109
109
  const DrawerContext: React.Context<DrawerState | null> = createContext(null);
@@ -146,15 +146,15 @@ export component DrawerRoot(
146
146
  const [isOpen, setOpen] = useControlled(open, defaultOpen, onOpenChange);
147
147
  const [snapIndex, setSnapIndex] = useControlled(snapPoint, defaultSnapPoint, onSnapPointChange);
148
148
  const bodyRef = useRef<HTMLElement | null>(null);
149
- const closes = useRef(0);
150
- const handles = useRef(0);
149
+ const closeCountRef = useRef(0);
150
+ const handleCountRef = useRef(0);
151
151
 
152
152
  const state = useMemo(
153
153
  () => ({
154
154
  bodyRef,
155
155
  close: () => setOpen(false),
156
- closes,
157
- handles,
156
+ closeCountRef,
157
+ handleCountRef,
158
158
  setSnapIndex,
159
159
  side,
160
160
  snapIndex,
@@ -243,10 +243,10 @@ export component DrawerBody(children: React.Node, render?: RenderProp, ...rest:
243
243
  */
244
244
  component RequireCloseForTheDrag() {
245
245
  const drawer = useDrawer("Drawer.Body");
246
- const { closes, handles } = drawer;
246
+ const { closeCountRef, handleCountRef } = drawer;
247
247
 
248
248
  useEffect(() => {
249
- if (handles.current > 0 && closes.current === 0) {
249
+ if (handleCountRef.current > 0 && closeCountRef.current === 0) {
250
250
  throw new Error(
251
251
  "Drawer.Body has a Drawer.Handle and no Drawer.Close: WCAG 2.2 SC 2.5.7 " +
252
252
  "requires anything achievable by dragging to be achievable without a " +
@@ -254,7 +254,7 @@ component RequireCloseForTheDrag() {
254
254
  "replacement for one.",
255
255
  );
256
256
  }
257
- }, [closes, handles]);
257
+ }, [closeCountRef, handleCountRef]);
258
258
 
259
259
  return null;
260
260
  }
@@ -303,14 +303,19 @@ export component DrawerDescription(children: React.Node, render?: RenderProp, ..
303
303
  */
304
304
  export component DrawerClose(children: React.Node, render?: RenderProp, ...rest: Rest) {
305
305
  const drawer = useDrawer("Drawer.Close");
306
- const closes = drawer.closes;
307
-
308
- useEffect(() => {
309
- closes.current += 1;
310
- return () => {
311
- closes.current -= 1;
312
- };
313
- }, [closes]);
306
+ const closeCountRef = drawer.closeCountRef;
307
+
308
+ useEffect(
309
+ () => {
310
+ closeCountRef.current += 1;
311
+ return () => {
312
+ closeCountRef.current -= 1;
313
+ };
314
+ },
315
+ // The context ref object is stable; the effect registers this part's mount.
316
+ // uf-lint-disable-next-line react-compiler/refs
317
+ [closeCountRef],
318
+ );
314
319
 
315
320
  return (
316
321
  <SheetClose {...forwarded(rest)} render={render}>
@@ -337,7 +342,7 @@ export component DrawerHandle(
337
342
  ...rest: Rest
338
343
  ) {
339
344
  const drawer = useDrawer("Drawer.Handle");
340
- const { bodyRef, close, handles, setSnapIndex, side, snapIndex, snapPoints } = drawer;
345
+ const { bodyRef, close, handleCountRef, setSnapIndex, side, snapIndex, snapPoints } = drawer;
341
346
  const passed = withoutComposed(rest, [
342
347
  "onKeyDown",
343
348
  "onPointerDown",
@@ -354,12 +359,17 @@ export component DrawerHandle(
354
359
  // So `Drawer.Body` knows there is a drag to provide an alternative to. A
355
360
  // drawer with no handle has no gesture, and requiring a close button of one
356
361
  // would be this component inventing a rule WCAG did not write.
357
- useEffect(() => {
358
- handles.current += 1;
359
- return () => {
360
- handles.current -= 1;
361
- };
362
- }, [handles]);
362
+ useEffect(
363
+ () => {
364
+ handleCountRef.current += 1;
365
+ return () => {
366
+ handleCountRef.current -= 1;
367
+ };
368
+ },
369
+ // The context ref object is stable; the effect registers this part's mount.
370
+ // uf-lint-disable-next-line react-compiler/refs
371
+ [handleCountRef],
372
+ );
363
373
 
364
374
  /** Move by one snap point, or close when there is no smaller one. */
365
375
  const step = (towardsOpen: boolean) => {
@@ -420,6 +430,8 @@ export component DrawerHandle(
420
430
  step(false);
421
431
  }
422
432
  }),
433
+ // Pointer handlers keep drag coordinates in refs between events.
434
+ // uf-lint-disable-next-line react-compiler/refs
423
435
  onPointerDown: composeHandlers(rest.onPointerDown, (event: $FlowFixMe) => {
424
436
  dragFrom.current = vertical ? event.clientY : event.clientX;
425
437
  setDragging(true);
@@ -427,6 +439,7 @@ export component DrawerHandle(
427
439
  // immediately: the handle moves with the drawer.
428
440
  event.currentTarget?.setPointerCapture?.(event.pointerId);
429
441
  }),
442
+ // uf-lint-disable-next-line react-compiler/refs
430
443
  onPointerMove: composeHandlers(rest.onPointerMove, (event: $FlowFixMe) => {
431
444
  const body = bodyRef.current;
432
445
  if (dragFrom.current == null || body == null) {
@@ -436,6 +449,7 @@ export component DrawerHandle(
436
449
  // otherwise lift it off the edge it is attached to.
437
450
  body.style.setProperty("--uf-drawer-drag", `${String(Math.max(0, travelled(event)))}px`);
438
451
  }),
452
+ // uf-lint-disable-next-line react-compiler/refs
439
453
  onPointerUp: composeHandlers(rest.onPointerUp, (event: $FlowFixMe) => {
440
454
  const body = bodyRef.current;
441
455
  const moved = travelled(event);
package/hover-card.js CHANGED
@@ -68,7 +68,7 @@ type HoverCardState = {|
68
68
  readonly openDelay: number,
69
69
  readonly closeDelay: number,
70
70
  /** Whether `Escape` has dismissed it; see `tooltip.js`, which shares the rule. */
71
- readonly dismissed: { current: boolean },
71
+ readonly dismissedRef: { current: boolean },
72
72
  |};
73
73
 
74
74
  const HoverCardContext: React.Context<HoverCardState | null> = createContext(null);
@@ -99,14 +99,14 @@ export component HoverCardRoot(
99
99
  const base = useId();
100
100
  const [isOpen, setOpen] = useControlled(open, defaultOpen, onOpenChange);
101
101
  const triggerRef = useRef<HTMLElement | null>(null);
102
- const dismissed = useRef(false);
102
+ const dismissedRef = useRef(false);
103
103
  const intent = useHoverIntent(setOpen);
104
104
 
105
105
  const state = useMemo(
106
106
  () => ({
107
107
  base,
108
108
  closeDelay,
109
- dismissed,
109
+ dismissedRef,
110
110
  intent,
111
111
  open: isOpen,
112
112
  openDelay,
@@ -129,36 +129,36 @@ export component HoverCardRoot(
129
129
  */
130
130
  export component HoverCardTrigger(children?: React.Node, render?: RenderProp, ...rest: Rest) {
131
131
  const card = useHoverCard("HoverCard.Trigger");
132
- const { closeDelay, dismissed, intent, openDelay, triggerRef } = card;
132
+ const { closeDelay, dismissedRef, intent, openDelay, triggerRef } = card;
133
133
  useFocusableTrigger(triggerRef, "HoverCard.Trigger");
134
134
 
135
135
  // A press focuses the trigger, and a card opening under the reader's own
136
136
  // click would cover what they just went to. See `tooltip.js`.
137
- const pressed = useRef(false);
137
+ const pressedRef = useRef(false);
138
138
 
139
139
  useEventListener(triggerRef, "pointerenter", (event: $FlowFixMe) => {
140
- if (event.pointerType === "touch" || dismissed.current) {
140
+ if (event.pointerType === "touch" || dismissedRef.current) {
141
141
  return;
142
142
  }
143
143
  intent.openAfter(openDelay);
144
144
  });
145
145
  useEventListener(triggerRef, "pointerleave", () => {
146
- dismissed.current = false;
146
+ dismissedRef.current = false;
147
147
  intent.closeAfter(closeDelay);
148
148
  });
149
149
  useEventListener(triggerRef, "pointerdown", () => {
150
- pressed.current = true;
150
+ pressedRef.current = true;
151
151
  intent.cancel();
152
152
  });
153
153
  useEventListener(triggerRef, "focusin", () => {
154
- if (pressed.current) {
155
- pressed.current = false;
154
+ if (pressedRef.current) {
155
+ pressedRef.current = false;
156
156
  return;
157
157
  }
158
158
  // The focus a dismissed card hands *back* to this trigger must not reopen
159
159
  // it, which is the whole reason the flag exists: without it `Escape` closes
160
160
  // the card, focus returns here, and the card comes straight back.
161
- if (dismissed.current) {
161
+ if (dismissedRef.current) {
162
162
  return;
163
163
  }
164
164
  // A reader who tabbed here has said what they want; only the pointer is
@@ -166,8 +166,8 @@ export component HoverCardTrigger(children?: React.Node, render?: RenderProp, ..
166
166
  intent.openAfter(0);
167
167
  });
168
168
  useEventListener(triggerRef, "focusout", () => {
169
- pressed.current = false;
170
- dismissed.current = false;
169
+ pressedRef.current = false;
170
+ dismissedRef.current = false;
171
171
  // `closeAfter` rather than a close, and this is where the delay earns its
172
172
  // keep a second time: `Tab` from the trigger *into* the card is a leave
173
173
  // followed immediately by an arrival, and the card's own `focusin` calls
@@ -179,6 +179,8 @@ export component HoverCardTrigger(children?: React.Node, render?: RenderProp, ..
179
179
 
180
180
  // Annotated because this one is not written inside a `ref={...}`, and there
181
181
  // is nothing else here for Flow to infer the element's type from.
182
+ // React calls callback refs during commit; pointer and focus handlers read it later.
183
+ // uf-lint-disable-next-line react-compiler/refs
182
184
  const attach = composeRefs(rest.ref, (element: HTMLElement | null) => {
183
185
  triggerRef.current = element;
184
186
  });
@@ -212,16 +214,16 @@ export component HoverCardBody(
212
214
  ...rest: Rest
213
215
  ) {
214
216
  const card = useHoverCard("HoverCard.Body");
215
- const { closeDelay, intent, open, triggerRef } = card;
217
+ const { closeDelay, dismissedRef, intent, open, triggerRef } = card;
216
218
  const bodyRef = useRef<HTMLElement | null>(null);
217
219
  // Whether the reader is *in* the card, as opposed to over it. It decides one
218
220
  // thing and it cannot be asked afterwards: a card closed while it held focus
219
221
  // has to hand focus back, and by the time the effect below is cleaned up the
220
222
  // element is gone from the document and `activeElement` has already fallen to
221
223
  // `<body>` — so the answer is kept while it is still true.
222
- const held = useRef(false);
224
+ const heldRef = useRef(false);
223
225
  const close = useStableCallback(() => {
224
- card.dismissed.current = true;
226
+ dismissedRef.current = true;
225
227
  intent.cancel();
226
228
  card.setOpen(false);
227
229
  });
@@ -250,11 +252,11 @@ export component HoverCardBody(
250
252
  const stay = () => intent.cancel();
251
253
  const go = () => intent.closeAfter(closeDelay);
252
254
  const arrived = () => {
253
- held.current = true;
255
+ heldRef.current = true;
254
256
  stay();
255
257
  };
256
258
  const gone = () => {
257
- held.current = false;
259
+ heldRef.current = false;
258
260
  go();
259
261
  };
260
262
  body.addEventListener("pointerenter", stay);
@@ -289,8 +291,8 @@ export component HoverCardBody(
289
291
  if (open) {
290
292
  return;
291
293
  }
292
- if (held.current) {
293
- held.current = false;
294
+ if (heldRef.current) {
295
+ heldRef.current = false;
294
296
  triggerRef.current?.focus?.();
295
297
  }
296
298
  }, [open, triggerRef]);
@@ -299,8 +301,8 @@ export component HoverCardBody(
299
301
  // the reader is inside it leaves focus on a node that is gone.
300
302
  useEffect(
301
303
  () => () => {
302
- if (held.current) {
303
- held.current = false;
304
+ if (heldRef.current) {
305
+ heldRef.current = false;
304
306
  triggerRef.current?.focus?.();
305
307
  }
306
308
  },
@@ -317,6 +319,8 @@ export component HoverCardBody(
317
319
  "data-side": anchored.side,
318
320
  "data-state": "open",
319
321
  id: `${card.base}-body`,
322
+ // React calls callback refs during commit; placement effects read it later.
323
+ // uf-lint-disable-next-line react-compiler/refs
320
324
  ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
321
325
  bodyRef.current = element;
322
326
  }),
package/interactions.js CHANGED
@@ -1776,6 +1776,8 @@ hook useDescription(text: string | void): string | void {
1776
1776
  const host: $FlowFixMe = globalThis;
1777
1777
  const body: HTMLElement | null = host.document?.body ?? null;
1778
1778
  if (text == null || text === "" || body == null) {
1779
+ // The id is state because it must render only after the shared DOM node exists.
1780
+ // uf-lint-disable-next-line react-compiler/set-state-in-effect
1779
1781
  setId(undefined);
1780
1782
  return;
1781
1783
  }
@@ -1795,6 +1797,8 @@ hook useDescription(text: string | void): string | void {
1795
1797
  }
1796
1798
  const entry: SharedDescription = shared;
1797
1799
  entry.users += 1;
1800
+ // The id is state because it must render only after the shared DOM node exists.
1801
+ // uf-lint-disable-next-line react-compiler/set-state-in-effect
1798
1802
  setId(entry.node.id);
1799
1803
  return () => {
1800
1804
  entry.users -= 1;