@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 +2 -0
- package/alert-dialog.js +2 -0
- package/avatar.js +4 -0
- package/calendar.js +8 -8
- package/collapsible.js +2 -0
- package/combobox.js +26 -7
- package/context-menu.js +12 -3
- package/date-picker.js +11 -0
- package/dialog.js +6 -0
- package/drawer.js +38 -24
- package/hover-card.js +26 -22
- package/interactions.js +4 -0
- package/internal/menu-tree.js +3 -3
- package/internal/roving-focus.js +2 -0
- package/menu.js +39 -6
- package/menubar.js +8 -1
- package/package.json +5 -5
- package/popover.js +23 -0
- package/radio-group.js +2 -0
- package/resizable.js +6 -0
- package/scroll-area.js +13 -13
- package/select.js +23 -10
- package/sidebar.js +9 -2
- package/skeleton.js +4 -0
- package/slider.js +6 -0
- package/toast.js +13 -11
- package/toggle-group.js +2 -0
- package/tooltip.js +19 -15
package/internal/menu-tree.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
219
|
+
pendingFocusRef,
|
|
220
220
|
parent,
|
|
221
221
|
triggered,
|
|
222
222
|
registerTrigger: setTriggered,
|
package/internal/roving-focus.js
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
23
|
-
"@uniflowed/hooks": "0.0.0-alpha.
|
|
24
|
-
"@uniflowed/react": "0.0.0-alpha.
|
|
25
|
-
"@uniflowed/state": "0.0.0-alpha.
|
|
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
|
|
75
|
+
readonly rememberedRef: { current: Offset },
|
|
76
76
|
/** Written by the viewport, read by every scrollbar. */
|
|
77
77
|
readonly report: () => void,
|
|
78
|
-
readonly
|
|
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
|
|
110
|
-
const
|
|
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
|
-
|
|
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
|
|
122
|
+
for (const scrollbar of scrollbarsRef.current) {
|
|
123
123
|
write(scrollbar, viewport);
|
|
124
124
|
}
|
|
125
125
|
},
|
|
126
|
-
|
|
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 {
|
|
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
|
-
|
|
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 } =
|
|
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,
|
|
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 =
|
|
229
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
678
|
+
const wanted = pendingLandingRef.current;
|
|
667
679
|
if (wanted != null) {
|
|
668
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
});
|