@proyecto-viviana/solidaria-components 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ActionBar.d.ts.map +1 -1
- package/dist/ActionBar.js +2 -11
- package/dist/ActionBar.js.map +1 -1
- package/dist/ActionBar.jsx +2 -11
- package/dist/ActionBar.jsx.map +1 -1
- package/dist/ActionGroup.d.ts.map +1 -1
- package/dist/ActionGroup.js +1 -1
- package/dist/ActionGroup.js.map +1 -1
- package/dist/ActionGroup.jsx +1 -1
- package/dist/ActionGroup.jsx.map +1 -1
- package/dist/Autocomplete.d.ts +2 -2
- package/dist/Autocomplete.d.ts.map +1 -1
- package/dist/Autocomplete.js +7 -7
- package/dist/Autocomplete.js.map +1 -1
- package/dist/Autocomplete.jsx +7 -7
- package/dist/Autocomplete.jsx.map +1 -1
- package/dist/Breadcrumbs.d.ts.map +1 -1
- package/dist/Breadcrumbs.js +18 -18
- package/dist/Breadcrumbs.js.map +1 -1
- package/dist/Breadcrumbs.jsx +21 -26
- package/dist/Breadcrumbs.jsx.map +1 -1
- package/dist/Calendar.d.ts.map +1 -1
- package/dist/Calendar.js +100 -65
- package/dist/Calendar.js.map +1 -1
- package/dist/Calendar.jsx +10 -1
- package/dist/Calendar.jsx.map +1 -1
- package/dist/Color.d.ts.map +1 -1
- package/dist/Color.js +15 -18
- package/dist/Color.js.map +1 -1
- package/dist/Color.jsx +15 -19
- package/dist/Color.jsx.map +1 -1
- package/dist/ComboBox.d.ts +4 -0
- package/dist/ComboBox.d.ts.map +1 -1
- package/dist/ComboBox.js +21 -13
- package/dist/ComboBox.js.map +1 -1
- package/dist/ComboBox.jsx +23 -13
- package/dist/ComboBox.jsx.map +1 -1
- package/dist/DateField.d.ts +11 -2
- package/dist/DateField.d.ts.map +1 -1
- package/dist/DateField2.js +237 -234
- package/dist/DateField2.js.map +1 -1
- package/dist/DateField2.jsx +134 -97
- package/dist/DateField2.jsx.map +1 -1
- package/dist/DatePicker.d.ts +42 -3
- package/dist/DatePicker.d.ts.map +1 -1
- package/dist/DatePicker.js +6 -990
- package/dist/DatePicker.jsx +6 -681
- package/dist/DatePicker2.js +1171 -0
- package/dist/DatePicker2.js.map +1 -0
- package/dist/DatePicker2.jsx +820 -0
- package/dist/DatePicker2.jsx.map +1 -0
- package/dist/DateRangePickerContext.d.ts +2 -0
- package/dist/DateRangePickerContext.d.ts.map +1 -1
- package/dist/Disclosure.d.ts.map +1 -1
- package/dist/Disclosure.js +3 -0
- package/dist/Disclosure.js.map +1 -1
- package/dist/Disclosure.jsx +1 -1
- package/dist/Disclosure.jsx.map +1 -1
- package/dist/ElementTag.d.ts +29 -0
- package/dist/ElementTag.d.ts.map +1 -0
- package/dist/ElementTag.js +220 -0
- package/dist/ElementTag.js.map +1 -0
- package/dist/ElementTag.jsx +67 -0
- package/dist/ElementTag.jsx.map +1 -0
- package/dist/GridList.d.ts.map +1 -1
- package/dist/GridList.js +2 -2
- package/dist/GridList.js.map +1 -1
- package/dist/GridList.jsx +2 -2
- package/dist/GridList.jsx.map +1 -1
- package/dist/Landmark.js +4 -3
- package/dist/Landmark.js.map +1 -1
- package/dist/Landmark.jsx +3 -3
- package/dist/Landmark.jsx.map +1 -1
- package/dist/Link.js +3 -2
- package/dist/Link.js.map +1 -1
- package/dist/Link.jsx +3 -3
- package/dist/Link.jsx.map +1 -1
- package/dist/ListBox.d.ts +2 -2
- package/dist/ListBox.d.ts.map +1 -1
- package/dist/ListBox.js +327 -166
- package/dist/ListBox.js.map +1 -1
- package/dist/ListBox.jsx +161 -25
- package/dist/ListBox.jsx.map +1 -1
- package/dist/Modal.d.ts.map +1 -1
- package/dist/Modal.js +7 -5
- package/dist/Modal.js.map +1 -1
- package/dist/Modal.jsx +5 -5
- package/dist/Modal.jsx.map +1 -1
- package/dist/Popover.d.ts.map +1 -1
- package/dist/Popover.js +16 -9
- package/dist/Popover.js.map +1 -1
- package/dist/Popover.jsx +25 -4
- package/dist/Popover.jsx.map +1 -1
- package/dist/RangeCalendar.d.ts.map +1 -1
- package/dist/RangeCalendar.js +182 -46
- package/dist/RangeCalendar.js.map +1 -1
- package/dist/RangeCalendar.jsx +72 -2
- package/dist/RangeCalendar.jsx.map +1 -1
- package/dist/SearchField.d.ts.map +1 -1
- package/dist/SearchField.js +26 -2
- package/dist/SearchField.js.map +1 -1
- package/dist/SearchField.jsx +26 -2
- package/dist/SearchField.jsx.map +1 -1
- package/dist/Select.d.ts +7 -0
- package/dist/Select.d.ts.map +1 -1
- package/dist/Select.js +64 -51
- package/dist/Select.js.map +1 -1
- package/dist/Select.jsx +35 -20
- package/dist/Select.jsx.map +1 -1
- package/dist/Separator.js +3 -2
- package/dist/Separator.js.map +1 -1
- package/dist/Separator.jsx +2 -2
- package/dist/Separator.jsx.map +1 -1
- package/dist/SharedElementTransition.d.ts.map +1 -1
- package/dist/SharedElementTransition.js +35 -33
- package/dist/SharedElementTransition.js.map +1 -1
- package/dist/SharedElementTransition.jsx +35 -33
- package/dist/SharedElementTransition.jsx.map +1 -1
- package/dist/StepList.d.ts.map +1 -1
- package/dist/StepList.js +2 -9
- package/dist/StepList.js.map +1 -1
- package/dist/StepList.jsx +1 -6
- package/dist/StepList.jsx.map +1 -1
- package/dist/Table.d.ts +2 -0
- package/dist/Table.d.ts.map +1 -1
- package/dist/Table.js +10 -3
- package/dist/Table.js.map +1 -1
- package/dist/Table.jsx +10 -3
- package/dist/Table.jsx.map +1 -1
- package/dist/Tabs.d.ts.map +1 -1
- package/dist/Tabs.js +3 -0
- package/dist/Tabs.js.map +1 -1
- package/dist/Tabs.jsx +1 -1
- package/dist/Tabs.jsx.map +1 -1
- package/dist/TagGroup.d.ts +3 -1
- package/dist/TagGroup.d.ts.map +1 -1
- package/dist/TagGroup.js +12 -8
- package/dist/TagGroup.js.map +1 -1
- package/dist/TagGroup.jsx +11 -8
- package/dist/TagGroup.jsx.map +1 -1
- package/dist/Text.d.ts.map +1 -1
- package/dist/Text.js +9 -8
- package/dist/Text.js.map +1 -1
- package/dist/Text.jsx +3 -3
- package/dist/Text.jsx.map +1 -1
- package/dist/TextField.js +4 -1
- package/dist/TextField.js.map +1 -1
- package/dist/TextField.jsx +4 -1
- package/dist/TextField.jsx.map +1 -1
- package/dist/TimeField.d.ts +14 -50
- package/dist/TimeField.d.ts.map +1 -1
- package/dist/TimeField.js +111 -205
- package/dist/TimeField.js.map +1 -1
- package/dist/TimeField.jsx +76 -82
- package/dist/TimeField.jsx.map +1 -1
- package/dist/Tree.d.ts.map +1 -1
- package/dist/Tree.js +3 -3
- package/dist/Tree.js.map +1 -1
- package/dist/Tree.jsx +3 -3
- package/dist/Tree.jsx.map +1 -1
- package/dist/VisuallyHidden.d.ts.map +1 -1
- package/dist/VisuallyHidden.js +3 -11
- package/dist/VisuallyHidden.js.map +1 -1
- package/dist/VisuallyHidden.jsx +3 -4
- package/dist/VisuallyHidden.jsx.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.jsx +6 -6
- package/dist/useDragAndDrop.d.ts +0 -8
- package/dist/useDragAndDrop.d.ts.map +1 -1
- package/dist/useDragAndDrop2.js +6 -2
- package/dist/useDragAndDrop2.js.map +1 -1
- package/dist/useDragAndDrop2.jsx +6 -2
- package/dist/useDragAndDrop2.jsx.map +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +2 -2
- package/dist/utils.js.map +1 -1
- package/dist/utils.jsx +2 -2
- package/dist/utils.jsx.map +1 -1
- package/package.json +3 -9
- package/src/ActionBar.tsx +10 -12
- package/src/ActionGroup.tsx +5 -3
- package/src/Autocomplete.tsx +14 -9
- package/src/Breadcrumbs.tsx +57 -42
- package/src/Calendar.tsx +30 -1
- package/src/Color.tsx +42 -27
- package/src/ComboBox.tsx +49 -17
- package/src/DateField.tsx +285 -52
- package/src/DatePicker.tsx +345 -57
- package/src/DateRangePickerContext.tsx +2 -0
- package/src/Disclosure.tsx +4 -0
- package/src/ElementTag.tsx +116 -0
- package/src/GridList.tsx +8 -2
- package/src/Landmark.tsx +4 -4
- package/src/Link.tsx +4 -4
- package/src/ListBox.tsx +323 -21
- package/src/Modal.tsx +21 -5
- package/src/Popover.tsx +28 -7
- package/src/RangeCalendar.tsx +120 -1
- package/src/SearchField.tsx +53 -1
- package/src/Select.tsx +114 -22
- package/src/Separator.tsx +3 -3
- package/src/SharedElementTransition.tsx +92 -36
- package/src/StepList.tsx +0 -7
- package/src/Table.tsx +19 -4
- package/src/Tabs.tsx +1 -0
- package/src/TagGroup.tsx +26 -6
- package/src/Text.tsx +5 -4
- package/src/TextField.tsx +1 -1
- package/src/TimeField.tsx +113 -196
- package/src/Tree.tsx +14 -4
- package/src/VisuallyHidden.tsx +8 -12
- package/src/index.ts +3 -8
- package/src/useDragAndDrop.ts +8 -10
- package/src/utils.tsx +3 -3
- package/dist/DatePicker.js.map +0 -1
- package/dist/DatePicker.jsx.map +0 -1
- package/dist/HiddenTimeInput.d.ts +0 -25
- package/dist/HiddenTimeInput.d.ts.map +0 -1
- package/dist/HiddenTimeInput.js +0 -104
- package/dist/HiddenTimeInput.js.map +0 -1
- package/dist/HiddenTimeInput.jsx +0 -69
- package/dist/HiddenTimeInput.jsx.map +0 -1
- package/src/HiddenTimeInput.tsx +0 -133
package/src/ListBox.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
createEffect,
|
|
12
12
|
createMemo,
|
|
13
13
|
createSignal,
|
|
14
|
+
on,
|
|
14
15
|
onCleanup,
|
|
15
16
|
splitProps,
|
|
16
17
|
useContext,
|
|
@@ -22,17 +23,29 @@ import {
|
|
|
22
23
|
createOption,
|
|
23
24
|
createFocusRing,
|
|
24
25
|
createScrollIntoViewOnFocus,
|
|
26
|
+
createStringFormatter,
|
|
27
|
+
createDragSession,
|
|
28
|
+
getGlobalDraggingCollectionRef,
|
|
29
|
+
getGlobalDraggingKeys,
|
|
30
|
+
registerDropItem,
|
|
31
|
+
dndIntlStrings,
|
|
25
32
|
mergeProps,
|
|
33
|
+
moveVirtualFocus,
|
|
26
34
|
useLocale,
|
|
35
|
+
FOCUS_EVENT,
|
|
36
|
+
CLEAR_FOCUS_EVENT,
|
|
27
37
|
type AriaListBoxProps,
|
|
28
38
|
type AriaOptionProps,
|
|
29
39
|
} from "@proyecto-viviana/solidaria";
|
|
30
40
|
import {
|
|
31
41
|
createListState,
|
|
42
|
+
createFilteredListState,
|
|
32
43
|
type ListState,
|
|
44
|
+
type ListFilterFn,
|
|
33
45
|
type Key,
|
|
34
46
|
type DropTarget,
|
|
35
47
|
} from "@proyecto-viviana/solid-stately";
|
|
48
|
+
import { useAutocompleteCollection } from "./Autocomplete";
|
|
36
49
|
import {
|
|
37
50
|
type RenderChildren,
|
|
38
51
|
type ClassNameOrFunction,
|
|
@@ -53,7 +66,10 @@ import {
|
|
|
53
66
|
mergePersistedKeysIntoVirtualRange,
|
|
54
67
|
useDndPersistedKeys,
|
|
55
68
|
useRenderDropIndicator,
|
|
69
|
+
DropIndicatorContext,
|
|
70
|
+
type DropIndicatorProps,
|
|
56
71
|
} from "./DragAndDrop";
|
|
72
|
+
import type { ItemDropTarget } from "@proyecto-viviana/solid-stately";
|
|
57
73
|
import {
|
|
58
74
|
CollectionRendererContext,
|
|
59
75
|
Section,
|
|
@@ -124,7 +140,7 @@ export interface ListBoxProps<T> extends Omit<AriaListBoxProps, "children">, Slo
|
|
|
124
140
|
/** Called when the load more sentinel becomes visible. */
|
|
125
141
|
onLoadMore?: () => void | Promise<void>;
|
|
126
142
|
/** Ref for the listbox element. */
|
|
127
|
-
ref?: RefLike<
|
|
143
|
+
ref?: RefLike<HTMLDivElement>;
|
|
128
144
|
/** Drag and drop hooks from `useDragAndDrop`. */
|
|
129
145
|
dragAndDropHooks?: DragAndDropHooks<T>;
|
|
130
146
|
/** Layout hint for styling parity. */
|
|
@@ -165,7 +181,7 @@ export interface ListBoxOptionProps<T>
|
|
|
165
181
|
/** The text value of the option (for typeahead). */
|
|
166
182
|
textValue?: string;
|
|
167
183
|
/** Ref for the option element. */
|
|
168
|
-
ref?: RefLike<
|
|
184
|
+
ref?: RefLike<HTMLDivElement>;
|
|
169
185
|
}
|
|
170
186
|
|
|
171
187
|
export interface ListBoxLoadMoreItemProps extends SlotProps {
|
|
@@ -245,7 +261,7 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
|
|
|
245
261
|
|
|
246
262
|
const hasSections = createMemo(() => stateProps.items.some((item) => isCollectionSection(item)));
|
|
247
263
|
|
|
248
|
-
const
|
|
264
|
+
const baseState = createListState<T>({
|
|
249
265
|
get items() {
|
|
250
266
|
return flatItems();
|
|
251
267
|
},
|
|
@@ -281,6 +297,21 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
|
|
|
281
297
|
},
|
|
282
298
|
});
|
|
283
299
|
|
|
300
|
+
// When this ListBox is the collection of an Autocomplete, the input and the
|
|
301
|
+
// collection are separate components bridged by context. The collection's
|
|
302
|
+
// state is filtered by the input's predicate (mirrors RAC's
|
|
303
|
+
// UNSTABLE_useFilteredListState), and virtual focus is driven across the
|
|
304
|
+
// component boundary by synthetic DOM events (see the bridge effects below).
|
|
305
|
+
// ComboBox/Picker never provide this context, so their createListBox path is
|
|
306
|
+
// untouched.
|
|
307
|
+
const autocompleteCtx = useAutocompleteCollection();
|
|
308
|
+
const state = autocompleteCtx
|
|
309
|
+
? createFilteredListState<T>(
|
|
310
|
+
baseState,
|
|
311
|
+
() => autocompleteCtx.filter as ListFilterFn<T> | undefined,
|
|
312
|
+
)
|
|
313
|
+
: baseState;
|
|
314
|
+
|
|
284
315
|
const resolveDisabled = (): boolean => {
|
|
285
316
|
const disabled = ariaProps.isDisabled;
|
|
286
317
|
if (typeof disabled === "function") {
|
|
@@ -290,9 +321,33 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
|
|
|
290
321
|
};
|
|
291
322
|
|
|
292
323
|
const locale = useLocale();
|
|
324
|
+
// A parent Virtualizer publishes `isVirtualized` through the collection
|
|
325
|
+
// renderer context; the base ListBox forwards it into createListBox so each
|
|
326
|
+
// option emits aria-posinset/aria-setsize for the windowed (incomplete) DOM.
|
|
327
|
+
const parentCollectionRenderer = useCollectionRenderer<unknown>();
|
|
293
328
|
const listBoxAria = createListBox(
|
|
294
329
|
{
|
|
295
330
|
...ariaProps,
|
|
331
|
+
get isVirtualized() {
|
|
332
|
+
return parentCollectionRenderer?.isVirtualized ?? ariaProps.isVirtualized;
|
|
333
|
+
},
|
|
334
|
+
// Under Autocomplete, the input owns the collection's id (its
|
|
335
|
+
// aria-controls target), accessible name, and virtual-focus/type-ahead
|
|
336
|
+
// config; prefer the bridged values over any locally-passed props.
|
|
337
|
+
get id() {
|
|
338
|
+
return autocompleteCtx?.collectionProps.id ?? (ariaProps as { id?: string }).id;
|
|
339
|
+
},
|
|
340
|
+
get "aria-label"() {
|
|
341
|
+
return autocompleteCtx?.collectionProps["aria-label"] ?? ariaProps["aria-label"];
|
|
342
|
+
},
|
|
343
|
+
get shouldUseVirtualFocus() {
|
|
344
|
+
return (
|
|
345
|
+
autocompleteCtx?.collectionProps.shouldUseVirtualFocus ?? ariaProps.shouldUseVirtualFocus
|
|
346
|
+
);
|
|
347
|
+
},
|
|
348
|
+
get disallowTypeAhead() {
|
|
349
|
+
return autocompleteCtx?.collectionProps.disallowTypeAhead ?? ariaProps.disallowTypeAhead;
|
|
350
|
+
},
|
|
296
351
|
get isDisabled() {
|
|
297
352
|
return resolveDisabled();
|
|
298
353
|
},
|
|
@@ -353,8 +408,81 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
|
|
|
353
408
|
ref: () => listRef(),
|
|
354
409
|
});
|
|
355
410
|
|
|
411
|
+
// Autocomplete bridge. createListBox reimplements navigation inline (it does
|
|
412
|
+
// not route through createSelectableCollection), so the virtual-focus event
|
|
413
|
+
// wiring the Autocomplete input relies on lives here, gated on the bridge
|
|
414
|
+
// context. This mirrors createSelectableCollection's FOCUS_EVENT/CLEAR_FOCUS_EVENT
|
|
415
|
+
// handlers and useSelectableItem's per-item moveVirtualFocus, at the component
|
|
416
|
+
// layer, without touching the shared hooks (protecting ComboBox/Picker).
|
|
417
|
+
if (autocompleteCtx) {
|
|
418
|
+
// Forward path: the input dispatches FOCUS_EVENT/CLEAR_FOCUS_EVENT onto the
|
|
419
|
+
// collection element. FOCUS_EVENT marks the collection focused (and, when the
|
|
420
|
+
// user types forward, requests the first item); the actual arrow navigation
|
|
421
|
+
// arrives as a re-dispatched KeyboardEvent handled by createListBox's onKeyDown.
|
|
422
|
+
let shouldVirtualFocusFirst = false;
|
|
423
|
+
createEffect(() => {
|
|
424
|
+
const list = listRef();
|
|
425
|
+
if (!list) return;
|
|
426
|
+
const onFocusEvent = (e: Event) => {
|
|
427
|
+
const detail = (e as CustomEvent).detail;
|
|
428
|
+
e.stopPropagation();
|
|
429
|
+
state.setFocused(true);
|
|
430
|
+
if (detail?.focusStrategy === "first") {
|
|
431
|
+
shouldVirtualFocusFirst = true;
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
const onClearFocusEvent = (e: Event) => {
|
|
435
|
+
e.stopPropagation();
|
|
436
|
+
state.setFocused(false);
|
|
437
|
+
if ((e as CustomEvent).detail?.clearFocusKey) {
|
|
438
|
+
state.setFocusedKey(null);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
list.addEventListener(FOCUS_EVENT, onFocusEvent);
|
|
442
|
+
list.addEventListener(CLEAR_FOCUS_EVENT, onClearFocusEvent);
|
|
443
|
+
onCleanup(() => {
|
|
444
|
+
list.removeEventListener(FOCUS_EVENT, onFocusEvent);
|
|
445
|
+
list.removeEventListener(CLEAR_FOCUS_EVENT, onClearFocusEvent);
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// Focus the first item once the (filtered) collection settles after the user
|
|
450
|
+
// types forward. If nothing survives the filter, clear the input's active
|
|
451
|
+
// descendant by moving virtual focus onto the collection itself (its focusin
|
|
452
|
+
// reaches the input's clear branch). Mirrors useSelectableCollection.
|
|
453
|
+
createEffect(
|
|
454
|
+
on(
|
|
455
|
+
() => [state.collection().getFirstKey?.() ?? null, state.collection().size] as const,
|
|
456
|
+
([firstKey, size]) => {
|
|
457
|
+
if (!shouldVirtualFocusFirst) return;
|
|
458
|
+
if (firstKey == null) {
|
|
459
|
+
const list = listRef();
|
|
460
|
+
if (list) moveVirtualFocus(list);
|
|
461
|
+
if (size > 0) shouldVirtualFocusFirst = false;
|
|
462
|
+
} else {
|
|
463
|
+
state.setFocusedKey(firstKey);
|
|
464
|
+
shouldVirtualFocusFirst = false;
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
{ defer: true },
|
|
468
|
+
),
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// Reverse path: mirror the focused key onto the option's DOM element via a
|
|
472
|
+
// synthetic, bubbling focusin (moveVirtualFocus). The input's focusin listener
|
|
473
|
+
// reads target.id into its aria-activedescendant. Option ids are String(key)
|
|
474
|
+
// in this path (createOption). Real DOM focus stays on the input.
|
|
475
|
+
createEffect(() => {
|
|
476
|
+
const key = state.focusedKey();
|
|
477
|
+
const list = listRef();
|
|
478
|
+
if (!list || !state.isFocused()) return;
|
|
479
|
+
if (key == null) return;
|
|
480
|
+
const el = list.ownerDocument.getElementById(String(key));
|
|
481
|
+
if (el) moveVirtualFocus(el);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
356
485
|
const isEmpty = () => stateProps.items.length === 0;
|
|
357
|
-
const parentCollectionRenderer = useCollectionRenderer<unknown>();
|
|
358
486
|
const getItemNodes = createMemo(() =>
|
|
359
487
|
Array.from(state.collection()).filter((node) => node.type === "item"),
|
|
360
488
|
);
|
|
@@ -432,6 +560,14 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
|
|
|
432
560
|
getKeyPageBelow: (key) => state.collection().getKeyAfter(key),
|
|
433
561
|
getKeyPageAbove: (key) => state.collection().getKeyBefore(key),
|
|
434
562
|
},
|
|
563
|
+
// The real collection drives keyboard drop-target navigation
|
|
564
|
+
// (`navigate()` walks getKeyAfter/getKeyBefore) and post-drop focus
|
|
565
|
+
// restoration. Reading it here re-registers the drop target when the
|
|
566
|
+
// collection changes (mirrors upstream keying its effect on the state).
|
|
567
|
+
collection: state.collection(),
|
|
568
|
+
selectedKeys: state.selectionManager.selectedKeys,
|
|
569
|
+
setSelectedKeys: (keys) => state.selectionManager.setSelectedKeys(keys),
|
|
570
|
+
setFocusedKey: (key) => state.setFocusedKey(key),
|
|
435
571
|
},
|
|
436
572
|
activeDropState,
|
|
437
573
|
() => listRef(),
|
|
@@ -448,6 +584,165 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
|
|
|
448
584
|
if (!target || target.type !== "item") return undefined;
|
|
449
585
|
return dndRenderDropIndicator()?.(target);
|
|
450
586
|
};
|
|
587
|
+
|
|
588
|
+
// ── Drop indicator (Fix B: labeled, self-focusing) ─────────────────────────
|
|
589
|
+
// The port of react-aria-components' ListBoxDropIndicatorWrapper +
|
|
590
|
+
// useDropIndicator. The droppable-collection state layer diverged to a minimal
|
|
591
|
+
// reactive target-holder with no collection reference, so the label (which
|
|
592
|
+
// needs the collection's text values) and the self-focus (which needs the live
|
|
593
|
+
// drag session) are computed here in the host layer, where both are available.
|
|
594
|
+
const dndFormatter = createStringFormatter(dndIntlStrings);
|
|
595
|
+
const dragSession = createDragSession();
|
|
596
|
+
const dropIndicatorTargetsEqual = (
|
|
597
|
+
a: DropTarget | null | undefined,
|
|
598
|
+
b: ItemDropTarget,
|
|
599
|
+
): boolean =>
|
|
600
|
+
!!a && a.type === "item" && a.key === b.key && a.dropPosition === b.dropPosition;
|
|
601
|
+
// Faithful port of useDropIndicator's label logic: "Drop on X" for an on-item
|
|
602
|
+
// target, else "Insert between X and Y" / "Insert before X" / "Insert after X"
|
|
603
|
+
// resolved from the target's neighbours in the collection.
|
|
604
|
+
const dropIndicatorLabel = (target: ItemDropTarget): string => {
|
|
605
|
+
const collection = state.collection();
|
|
606
|
+
const getText = (key: Key | null): string => {
|
|
607
|
+
if (key == null) return "";
|
|
608
|
+
return collection.getTextValue(key) ?? collection.getItem(key)?.textValue ?? "";
|
|
609
|
+
};
|
|
610
|
+
if (target.dropPosition === "on") {
|
|
611
|
+
return dndFormatter().format("dropOnItem", { itemText: getText(target.key) });
|
|
612
|
+
}
|
|
613
|
+
let before: Key | null;
|
|
614
|
+
let after: Key | null;
|
|
615
|
+
if (target.dropPosition === "before") {
|
|
616
|
+
const prevKey = collection.getKeyBefore(target.key);
|
|
617
|
+
const prevNode = prevKey != null ? collection.getItem(prevKey) : null;
|
|
618
|
+
before = prevNode?.type === "item" ? prevNode.key : null;
|
|
619
|
+
} else {
|
|
620
|
+
before = target.key;
|
|
621
|
+
}
|
|
622
|
+
if (target.dropPosition === "after") {
|
|
623
|
+
const nextKey = collection.getKeyAfter(target.key);
|
|
624
|
+
const nextNode = nextKey != null ? collection.getItem(nextKey) : null;
|
|
625
|
+
after = nextNode?.type === "item" ? nextNode.key : null;
|
|
626
|
+
} else {
|
|
627
|
+
after = target.key;
|
|
628
|
+
}
|
|
629
|
+
if (before != null && after != null) {
|
|
630
|
+
return dndFormatter().format("insertBetween", {
|
|
631
|
+
beforeItemText: getText(before),
|
|
632
|
+
afterItemText: getText(after),
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
if (before != null) {
|
|
636
|
+
return dndFormatter().format("insertAfter", { itemText: getText(before) });
|
|
637
|
+
}
|
|
638
|
+
if (after != null) {
|
|
639
|
+
return dndFormatter().format("insertBefore", { itemText: getText(after) });
|
|
640
|
+
}
|
|
641
|
+
return "";
|
|
642
|
+
};
|
|
643
|
+
// Wrap the raw Set<string> the DragManager hands drop items into the DragTypes
|
|
644
|
+
// delegate the port state expects (mirrors createDroppableCollection.wrapTypes).
|
|
645
|
+
const wrapDropTypes = (types: Set<string>) => ({
|
|
646
|
+
has: (type: string | string[]) => {
|
|
647
|
+
if (typeof type === "string") return types.has(type);
|
|
648
|
+
if (Array.isArray(type)) return type.some((t) => typeof t === "string" && types.has(t));
|
|
649
|
+
return false;
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
const ListBoxDropIndicator = (props: { target: ItemDropTarget }): JSX.Element => {
|
|
653
|
+
const [indicatorRef, setIndicatorRef] = createSignal<HTMLDivElement | null>(null);
|
|
654
|
+
const isDropTarget = createMemo(() =>
|
|
655
|
+
dropIndicatorTargetsEqual(dropState()?.target, props.target),
|
|
656
|
+
);
|
|
657
|
+
// aria-hidden mirrors useDropIndicator: outside a drag session, or on a
|
|
658
|
+
// non-active target, the indicator is hidden from AT ('true'); the active
|
|
659
|
+
// target during a session is exposed (undefined). isHidden then tells us
|
|
660
|
+
// whether to omit the element entirely (RAC returns null when hidden).
|
|
661
|
+
const ariaHidden = createMemo(() =>
|
|
662
|
+
!dragSession() ? "true" : isDropTarget() ? undefined : "true",
|
|
663
|
+
);
|
|
664
|
+
const isHidden = createMemo(() => !isDropTarget() && !!ariaHidden());
|
|
665
|
+
// Faithful port of useDroppableItem. Registration (of the indicator element as
|
|
666
|
+
// a DragManager droppable item) and the self-focus must happen in one effect,
|
|
667
|
+
// registration first: the DragManager's onFocus keys off the dropItems map, so
|
|
668
|
+
// if the element self-focused before it was registered, onFocus would treat it
|
|
669
|
+
// as an unknown target and redirect focus back to the collection (a pickup
|
|
670
|
+
// bounce). React's two useDroppableItem effects run in guaranteed order after
|
|
671
|
+
// the ref is attached; Solid's independent effects race, so we fuse them. The
|
|
672
|
+
// dropItems map keys off live elements — the ref nulls itself on Show cleanup
|
|
673
|
+
// (below), so a hidden indicator unregisters instead of lingering as a stale
|
|
674
|
+
// detached entry.
|
|
675
|
+
createEffect(() => {
|
|
676
|
+
const el = indicatorRef();
|
|
677
|
+
if (!el) return;
|
|
678
|
+
const unregister = registerDropItem({
|
|
679
|
+
element: el,
|
|
680
|
+
target: props.target,
|
|
681
|
+
getDropOperation: (types, allowedOperations) => {
|
|
682
|
+
const st = dropState();
|
|
683
|
+
if (!st) return "cancel";
|
|
684
|
+
// Thread the same drag identity the collection uses (see
|
|
685
|
+
// createDroppableCollection.isInternalDropOperation): an internal
|
|
686
|
+
// keyboard reorder must report `isInternal` so the state's feature
|
|
687
|
+
// detection keeps before/after indicators valid (and rejects "on" for a
|
|
688
|
+
// reorder-only host). Without it every indicator would return "cancel"
|
|
689
|
+
// and drop out of the DragManager's ariaHideOutside keep-set.
|
|
690
|
+
const isInternal = getGlobalDraggingCollectionRef() === listRef();
|
|
691
|
+
return st.getDropOperation(
|
|
692
|
+
props.target,
|
|
693
|
+
wrapDropTypes(types) as Parameters<typeof st.getDropOperation>[1],
|
|
694
|
+
allowedOperations,
|
|
695
|
+
isInternal,
|
|
696
|
+
getGlobalDraggingKeys(),
|
|
697
|
+
);
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
onCleanup(unregister);
|
|
701
|
+
// While a drag session is active, the current drop target moves real DOM
|
|
702
|
+
// focus onto itself, so keyboard drop navigation lands on the labeled
|
|
703
|
+
// insertion point rather than the collection element. The self-focus is
|
|
704
|
+
// deferred to an animation frame to mirror upstream's timing: in
|
|
705
|
+
// useDroppableItem the focus is a *passive* useEffect, so it commits only
|
|
706
|
+
// after the browser's microtask checkpoint. That ordering matters because
|
|
707
|
+
// the DragManager keeps the active drop indicator visible via
|
|
708
|
+
// ariaHideOutside, whose keep-set is `[dragSource, ...registered drop
|
|
709
|
+
// items]` (the collection is dropped from the set once it owns items). A
|
|
710
|
+
// freshly-mounted indicator is momentarily `inert` — it only leaves the
|
|
711
|
+
// keep-set's blind spot once the DragManager's MutationObserver re-runs
|
|
712
|
+
// updateValidDropTargets on a later microtask. Focusing synchronously (or
|
|
713
|
+
// on a microtask) would call .focus() while the element is still inert and
|
|
714
|
+
// silently no-op; the animation frame runs after that refresh, exactly as
|
|
715
|
+
// React's passive effect does.
|
|
716
|
+
if (dragSession() && isDropTarget()) {
|
|
717
|
+
requestAnimationFrame(() => {
|
|
718
|
+
if (dragSession() && isDropTarget() && indicatorRef() === el) {
|
|
719
|
+
el.focus();
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
return (
|
|
725
|
+
<Show when={!isHidden()}>
|
|
726
|
+
<div
|
|
727
|
+
ref={(el) => {
|
|
728
|
+
setIndicatorRef(el);
|
|
729
|
+
onCleanup(() => setIndicatorRef(null));
|
|
730
|
+
}}
|
|
731
|
+
role="option"
|
|
732
|
+
class="solidaria-DropIndicator"
|
|
733
|
+
aria-roledescription={dndFormatter().format("dropIndicator")}
|
|
734
|
+
aria-label={dropIndicatorLabel(props.target)}
|
|
735
|
+
aria-hidden={ariaHidden()}
|
|
736
|
+
tabindex={-1}
|
|
737
|
+
data-drop-target={isDropTarget() || undefined}
|
|
738
|
+
/>
|
|
739
|
+
</Show>
|
|
740
|
+
);
|
|
741
|
+
};
|
|
742
|
+
const dropIndicatorContextValue = {
|
|
743
|
+
render: (p: DropIndicatorProps) => <ListBoxDropIndicator target={p.target} />,
|
|
744
|
+
};
|
|
745
|
+
|
|
451
746
|
const virtualizer = useVirtualizerContext();
|
|
452
747
|
const persistedKeys = useDndPersistedKeys(
|
|
453
748
|
{ focusedKey: state.focusedKey },
|
|
@@ -565,11 +860,12 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
|
|
|
565
860
|
>
|
|
566
861
|
<ListBoxStateContext.Provider value={state}>
|
|
567
862
|
<CollectionRendererContext.Provider value={collectionRenderer()}>
|
|
863
|
+
<DropIndicatorContext.Provider value={dropIndicatorContextValue}>
|
|
568
864
|
<>
|
|
569
865
|
<Show when={ariaProps.label}>
|
|
570
866
|
<span {...cleanLabelProps()}>{ariaProps.label as JSX.Element}</span>
|
|
571
867
|
</Show>
|
|
572
|
-
<
|
|
868
|
+
<div
|
|
573
869
|
{...mergeProps(
|
|
574
870
|
domProps(),
|
|
575
871
|
cleanListBoxProps(),
|
|
@@ -580,6 +876,7 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
|
|
|
580
876
|
ref={(el) => {
|
|
581
877
|
setListRef(el);
|
|
582
878
|
assignRef(local.ref, el);
|
|
879
|
+
autocompleteCtx?.collectionRef(el);
|
|
583
880
|
}}
|
|
584
881
|
class={renderProps.class()}
|
|
585
882
|
style={renderProps.style()}
|
|
@@ -594,14 +891,14 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
|
|
|
594
891
|
>
|
|
595
892
|
<SharedElementTransition>
|
|
596
893
|
{isEmpty() && local.renderEmptyState ? (
|
|
597
|
-
<
|
|
894
|
+
<div role="option" style={{ display: "contents" }} data-empty-state>
|
|
598
895
|
{local.renderEmptyState()}
|
|
599
|
-
</
|
|
896
|
+
</div>
|
|
600
897
|
) : hasSections() ? (
|
|
601
898
|
<For each={sectionedRenderEntries()}>
|
|
602
899
|
{(entry) =>
|
|
603
900
|
entry.type === "section" ? (
|
|
604
|
-
<
|
|
901
|
+
<div role="presentation" data-section-wrapper>
|
|
605
902
|
<Section class="solidaria-ListBox-section">
|
|
606
903
|
{entry.section.title != null && (
|
|
607
904
|
<Header class="solidaria-ListBox-sectionHeader">
|
|
@@ -609,7 +906,7 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
|
|
|
609
906
|
</Header>
|
|
610
907
|
)}
|
|
611
908
|
<Group class="solidaria-ListBox-sectionGroup">
|
|
612
|
-
<
|
|
909
|
+
<div role="group" aria-label={entry.section["aria-label"]}>
|
|
613
910
|
<For each={entry.items}>
|
|
614
911
|
{(indexedItem) => (
|
|
615
912
|
<>
|
|
@@ -629,10 +926,10 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
|
|
|
629
926
|
</>
|
|
630
927
|
)}
|
|
631
928
|
</For>
|
|
632
|
-
</
|
|
929
|
+
</div>
|
|
633
930
|
</Group>
|
|
634
931
|
</Section>
|
|
635
|
-
</
|
|
932
|
+
</div>
|
|
636
933
|
) : (
|
|
637
934
|
<>
|
|
638
935
|
{collectionRenderer().renderDropIndicator?.(entry.item.index, "before")}
|
|
@@ -646,7 +943,7 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
|
|
|
646
943
|
) : (
|
|
647
944
|
<>
|
|
648
945
|
{virtualRange()?.offsetTop ? (
|
|
649
|
-
<
|
|
946
|
+
<div
|
|
650
947
|
role="presentation"
|
|
651
948
|
aria-hidden="true"
|
|
652
949
|
style={virtualSpacerStyle(virtualRange()!.offsetTop)}
|
|
@@ -673,7 +970,7 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
|
|
|
673
970
|
}}
|
|
674
971
|
</For>
|
|
675
972
|
{virtualRange()?.offsetBottom ? (
|
|
676
|
-
<
|
|
973
|
+
<div
|
|
677
974
|
role="presentation"
|
|
678
975
|
aria-hidden="true"
|
|
679
976
|
style={virtualSpacerStyle(virtualRange()!.offsetBottom)}
|
|
@@ -686,8 +983,9 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
|
|
|
686
983
|
{local.hasMore && local.onLoadMore && (
|
|
687
984
|
<ListBoxLoadMoreItem onLoadMore={local.onLoadMore} isLoading={local.isLoading} />
|
|
688
985
|
)}
|
|
689
|
-
</
|
|
986
|
+
</div>
|
|
690
987
|
</>
|
|
988
|
+
</DropIndicatorContext.Provider>
|
|
691
989
|
</CollectionRendererContext.Provider>
|
|
692
990
|
</ListBoxStateContext.Provider>
|
|
693
991
|
</ListBoxContext.Provider>
|
|
@@ -714,7 +1012,7 @@ export function ListBoxOption<T>(props: ListBoxOptionProps<T>): JSX.Element {
|
|
|
714
1012
|
}
|
|
715
1013
|
const state = context as ListState<T>;
|
|
716
1014
|
const listContext = useContext(ListBoxContext) as ListBoxContextValue<T> | null;
|
|
717
|
-
const [ref, setRef] = createSignal<
|
|
1015
|
+
const [ref, setRef] = createSignal<HTMLDivElement | null>(null);
|
|
718
1016
|
|
|
719
1017
|
const optionAria = createOption<T>(
|
|
720
1018
|
{
|
|
@@ -742,6 +1040,7 @@ export function ListBoxOption<T>(props: ListBoxOptionProps<T>): JSX.Element {
|
|
|
742
1040
|
},
|
|
743
1041
|
},
|
|
744
1042
|
state,
|
|
1043
|
+
ref,
|
|
745
1044
|
);
|
|
746
1045
|
|
|
747
1046
|
const renderValues = createMemo<ListBoxOptionRenderProps>(() => ({
|
|
@@ -777,6 +1076,9 @@ export function ListBoxOption<T>(props: ListBoxOptionProps<T>): JSX.Element {
|
|
|
777
1076
|
return listContext.dragAndDropHooks.useDraggableItem(
|
|
778
1077
|
{
|
|
779
1078
|
key: local.id as string | number,
|
|
1079
|
+
// Surfaces the drag-affordance description (aria-describedby) only for
|
|
1080
|
+
// selectable collections, mirroring upstream useDraggableItem.
|
|
1081
|
+
selectionMode: state.selectionManager.selectionMode,
|
|
780
1082
|
},
|
|
781
1083
|
listContext.dragState as Parameters<NonNullable<DragAndDropHooks<T>["useDraggableItem"]>>[1],
|
|
782
1084
|
);
|
|
@@ -808,7 +1110,7 @@ export function ListBoxOption<T>(props: ListBoxOptionProps<T>): JSX.Element {
|
|
|
808
1110
|
|
|
809
1111
|
return (
|
|
810
1112
|
<SelectionIndicatorContext.Provider value={selectionIndicatorContext()}>
|
|
811
|
-
<
|
|
1113
|
+
<div
|
|
812
1114
|
ref={(el) => {
|
|
813
1115
|
setRef(el);
|
|
814
1116
|
assignRef(local.ref, el);
|
|
@@ -836,7 +1138,7 @@ export function ListBoxOption<T>(props: ListBoxOptionProps<T>): JSX.Element {
|
|
|
836
1138
|
) : (
|
|
837
1139
|
renderProps.renderChildren()
|
|
838
1140
|
)}
|
|
839
|
-
</
|
|
1141
|
+
</div>
|
|
840
1142
|
</SelectionIndicatorContext.Provider>
|
|
841
1143
|
);
|
|
842
1144
|
}
|
|
@@ -892,10 +1194,10 @@ export function ListBoxLoadMoreItem(props: ListBoxLoadMoreItemProps): JSX.Elemen
|
|
|
892
1194
|
|
|
893
1195
|
return (
|
|
894
1196
|
<>
|
|
895
|
-
<
|
|
1197
|
+
<div style={{ position: "relative", width: 0, height: 0, overflow: "hidden" }} inert>
|
|
896
1198
|
<div ref={sentinelRef} style={{ position: "absolute", height: "1px", width: "1px" }} />
|
|
897
|
-
</
|
|
898
|
-
<
|
|
1199
|
+
</div>
|
|
1200
|
+
<div
|
|
899
1201
|
role="option"
|
|
900
1202
|
aria-disabled={true}
|
|
901
1203
|
tabIndex={0}
|
|
@@ -907,7 +1209,7 @@ export function ListBoxLoadMoreItem(props: ListBoxLoadMoreItemProps): JSX.Elemen
|
|
|
907
1209
|
data-loading={isLoading() || undefined}
|
|
908
1210
|
>
|
|
909
1211
|
{renderProps.renderChildren()}
|
|
910
|
-
</
|
|
1212
|
+
</div>
|
|
911
1213
|
</>
|
|
912
1214
|
);
|
|
913
1215
|
}
|
package/src/Modal.tsx
CHANGED
|
@@ -264,9 +264,14 @@ export function ModalOverlay(props: ModalOverlayProps): JSX.Element {
|
|
|
264
264
|
}
|
|
265
265
|
};
|
|
266
266
|
|
|
267
|
-
|
|
267
|
+
// Bubble phase, mirroring react-aria's `useOverlay` (its `onKeyDown` is an
|
|
268
|
+
// overlay-element prop, handled on bubble). Capture would preempt inner
|
|
269
|
+
// widgets that dismiss on Escape (a combobox/menu inside the dialog must
|
|
270
|
+
// consume Escape first) and would also fire before the event is recorded by
|
|
271
|
+
// capture-phase listeners, inverting the keydown/onOpenChange order.
|
|
272
|
+
document.addEventListener("keydown", handleKeyDown, false);
|
|
268
273
|
onCleanup(() => {
|
|
269
|
-
document.removeEventListener("keydown", handleKeyDown,
|
|
274
|
+
document.removeEventListener("keydown", handleKeyDown, false);
|
|
270
275
|
});
|
|
271
276
|
});
|
|
272
277
|
|
|
@@ -487,9 +492,11 @@ function ModalContent(props: ModalProps): JSX.Element {
|
|
|
487
492
|
}
|
|
488
493
|
};
|
|
489
494
|
|
|
490
|
-
|
|
495
|
+
// Bubble phase — see the ModalOverlay handler above for why (react-aria
|
|
496
|
+
// `useOverlay` parity; inner-widget Escape precedence; event-order fidelity).
|
|
497
|
+
document.addEventListener("keydown", handleKeyDown, false);
|
|
491
498
|
onCleanup(() => {
|
|
492
|
-
document.removeEventListener("keydown", handleKeyDown,
|
|
499
|
+
document.removeEventListener("keydown", handleKeyDown, false);
|
|
493
500
|
});
|
|
494
501
|
});
|
|
495
502
|
|
|
@@ -548,7 +555,16 @@ function ModalContent(props: ModalProps): JSX.Element {
|
|
|
548
555
|
// No autoFocus: react-aria's Overlay renders FocusScope with restoreFocus +
|
|
549
556
|
// contain only — initial focus is useDialog's job (it focuses the dialog
|
|
550
557
|
// element itself when nothing inside holds focus).
|
|
551
|
-
|
|
558
|
+
//
|
|
559
|
+
// `contain` is gated on `!isExiting`, mirroring react-aria's Overlay
|
|
560
|
+
// (react-aria/src/overlays/Overlay.tsx: `contain={... && !isExiting}`). The
|
|
561
|
+
// overlay stays mounted through its exit transition (isOpen || isExiting), so
|
|
562
|
+
// without this gate containment would remain active during the exit and yank
|
|
563
|
+
// focus back into the dialog as restoreFocus moves it to the trigger —
|
|
564
|
+
// producing a focus bounce (focusout→focusin→focusout) that React never
|
|
565
|
+
// shows. Releasing containment the instant the exit begins lets focus settle
|
|
566
|
+
// on the trigger in a single move, matching upstream's event sequence.
|
|
567
|
+
<FocusScope contain={!isModalExiting()} restoreFocus>
|
|
552
568
|
<div
|
|
553
569
|
{...domProps()}
|
|
554
570
|
ref={registerModalRef}
|
package/src/Popover.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
createOverlayTrigger,
|
|
23
23
|
createPopover,
|
|
24
24
|
FocusScope,
|
|
25
|
+
useLocale,
|
|
25
26
|
useUNSAFE_PortalContext,
|
|
26
27
|
visuallyHiddenStyles,
|
|
27
28
|
type AriaLabelingProps,
|
|
@@ -280,12 +281,29 @@ export function Popover(props: PopoverProps): JSX.Element {
|
|
|
280
281
|
"isExiting",
|
|
281
282
|
]);
|
|
282
283
|
|
|
283
|
-
let
|
|
284
|
+
// A reactive ref (not a plain `let`) so the overlay-position effect in
|
|
285
|
+
// createOverlayPosition — which tracks `overlayRef()` as a dependency —
|
|
286
|
+
// re-runs once the portal node actually mounts. React gets this timing for
|
|
287
|
+
// free from useLayoutEffect (the ref is populated before the position effect
|
|
288
|
+
// fires); in Solid the portal node is created lazily on open, AFTER the
|
|
289
|
+
// position effect first ran with a null ref, so a non-reactive ref left the
|
|
290
|
+
// popover stranded at the createOverlayPosition fallback (position:fixed;
|
|
291
|
+
// top:0; left:0) whenever no other dependency happened to re-fire the effect
|
|
292
|
+
// after mount. Mirrors the sibling groupRef signal below.
|
|
293
|
+
const [popoverRef, setPopoverRef] = createSignal<HTMLDivElement | null>(null);
|
|
284
294
|
const [groupRef, setGroupRef] = createSignal<HTMLDivElement | null>(null);
|
|
285
295
|
// False on the server and during hydration; true after onMount. Gates the Portal
|
|
286
296
|
// so overlay content only ever renders client-side, post-hydration.
|
|
287
297
|
const isHydrated = useIsHydrated();
|
|
288
298
|
|
|
299
|
+
// Mirrors upstream S2 Popover (Popover.mjs: `el.lang = locale; el.dir =
|
|
300
|
+
// direction`): the overlay portals out of the app root, so it must carry both
|
|
301
|
+
// the locale `lang` AND writing `direction` — otherwise the portaled
|
|
302
|
+
// listbox/dialog stays LTR and keeps the Latin font under an RTL Provider (the
|
|
303
|
+
// `:lang(ar)` font swap needs a `lang` ancestor the portal would otherwise
|
|
304
|
+
// escape). RAC's own Popover only threads `dir`; S2 adds `lang` on top.
|
|
305
|
+
const locale = useLocale();
|
|
306
|
+
|
|
289
307
|
const triggerContext = useContext(PopoverTriggerContext);
|
|
290
308
|
const dialogTriggerContext = useContext(DialogTriggerContext);
|
|
291
309
|
const popoverGroupContext = useContext(PopoverGroupContext);
|
|
@@ -333,7 +351,7 @@ export function Popover(props: PopoverProps): JSX.Element {
|
|
|
333
351
|
const popoverAria = createPopover(
|
|
334
352
|
{
|
|
335
353
|
triggerRef: getTriggerRef,
|
|
336
|
-
popoverRef: () => popoverRef
|
|
354
|
+
popoverRef: () => popoverRef(),
|
|
337
355
|
groupRef: () => (isSubPopover() ? (popoverGroupContext?.() ?? null) : groupRef()),
|
|
338
356
|
get placement() {
|
|
339
357
|
return local.placement;
|
|
@@ -514,7 +532,7 @@ export function Popover(props: PopoverProps): JSX.Element {
|
|
|
514
532
|
createEffect(() => {
|
|
515
533
|
if (!isOpen() || !shouldBeDialog()) return;
|
|
516
534
|
if ((local.autoFocus ?? true) === false) return;
|
|
517
|
-
if (!popoverRef) return;
|
|
535
|
+
if (!popoverRef()) return;
|
|
518
536
|
if (resolvedTrigger() === "SubmenuTrigger") return;
|
|
519
537
|
|
|
520
538
|
let timeout: number | undefined;
|
|
@@ -522,11 +540,12 @@ export function Popover(props: PopoverProps): JSX.Element {
|
|
|
522
540
|
|
|
523
541
|
const focusIfNeeded = () => {
|
|
524
542
|
if (!isOpen() || !shouldBeDialog()) return;
|
|
525
|
-
|
|
526
|
-
if (
|
|
543
|
+
const node = popoverRef();
|
|
544
|
+
if (!node || resolvedTrigger() === "SubmenuTrigger") return;
|
|
545
|
+
if (document.activeElement === node || node.contains(document.activeElement)) {
|
|
527
546
|
return;
|
|
528
547
|
}
|
|
529
|
-
|
|
548
|
+
node.focus();
|
|
530
549
|
};
|
|
531
550
|
|
|
532
551
|
const scheduleFocus = () => {
|
|
@@ -572,12 +591,14 @@ export function Popover(props: PopoverProps): JSX.Element {
|
|
|
572
591
|
<div
|
|
573
592
|
{...domProps()}
|
|
574
593
|
{...cleanPopoverProps()}
|
|
575
|
-
ref={
|
|
594
|
+
ref={setPopoverRef}
|
|
576
595
|
id={overlayId()}
|
|
577
596
|
role={shouldBeDialog() ? "dialog" : undefined}
|
|
578
597
|
tabIndex={shouldBeDialog() ? -1 : undefined}
|
|
579
598
|
class={renderProps.class()}
|
|
580
599
|
style={mergedStyle()}
|
|
600
|
+
lang={locale().locale}
|
|
601
|
+
dir={locale().direction}
|
|
581
602
|
data-trigger={resolvedTrigger()}
|
|
582
603
|
data-placement={popoverAria.placement()}
|
|
583
604
|
data-entering={dataAttr(local.isEntering)}
|