Skip to content

Commit e07ab9f

Browse files
committed
broken list items, need to figure out how to better sync updates
1 parent c86bd31 commit e07ab9f

File tree

17 files changed

+1343
-52
lines changed

17 files changed

+1343
-52
lines changed

packages/floating-ui-svelte/src/components/floating-focus-manager/floating-focus-manager.svelte

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,6 @@
411411
relatedTarget &&
412412
movedToUnrelatedNode &&
413413
!isPointerDown &&
414-
// Fix React 18 Strict Mode returnFocus due to double rendering.
415414
relatedTarget !== getPreviouslyFocusedElement()
416415
) {
417416
preventReturnFocus = true;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<script lang="ts" module>
2+
import { type Snippet } from "svelte";
3+
import { SvelteMap } from "svelte/reactivity";
4+
import { FloatingListContext } from "./hooks.svelte.js";
5+
6+
function sortByDocumentPosition(a: Node, b: Node) {
7+
const position = a.compareDocumentPosition(b);
8+
9+
if (
10+
position & Node.DOCUMENT_POSITION_FOLLOWING ||
11+
position & Node.DOCUMENT_POSITION_CONTAINED_BY
12+
) {
13+
return -1;
14+
}
15+
16+
if (
17+
position & Node.DOCUMENT_POSITION_PRECEDING ||
18+
position & Node.DOCUMENT_POSITION_CONTAINS
19+
) {
20+
return 1;
21+
}
22+
23+
return 0;
24+
}
25+
26+
interface FloatingListProps {
27+
children: Snippet;
28+
29+
/**
30+
* A reference to the list of HTMLElements, ordered by their index.
31+
* `useListNavigation's` `listRef` prop.
32+
*/
33+
elements: Array<HTMLElement | null>;
34+
35+
/**
36+
* A ref to the list of element labels, ordered by their index.
37+
* `useTypeahead`'s `listRef` prop.
38+
*/
39+
labels?: Array<string | null>;
40+
}
41+
</script>
42+
43+
<script lang="ts">
44+
let {
45+
children,
46+
elements = $bindable(),
47+
labels,
48+
}: FloatingListProps = $props();
49+
50+
let map = new SvelteMap<Node, number | null>();
51+
52+
function register(node: Node) {
53+
map.set(node, null);
54+
}
55+
56+
function unregister(node: Node) {
57+
map.delete(node);
58+
}
59+
60+
$effect.pre(() => {
61+
const nodes = Array.from(map.keys()).sort(sortByDocumentPosition);
62+
63+
nodes.forEach((node, index) => {
64+
map.set(node, index);
65+
});
66+
});
67+
68+
FloatingListContext.set({
69+
elements,
70+
labels,
71+
map,
72+
register,
73+
unregister,
74+
});
75+
</script>
76+
77+
{@render children()}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { SvelteMap } from "svelte/reactivity";
2+
import { extract } from "../../internal/extract.js";
3+
import type { MaybeGetter } from "../../types.js";
4+
import { Context } from "../../internal/context.js";
5+
import { watch } from "../../internal/watch.svelte.js";
6+
7+
type FloatingListContextType = {
8+
register: (node: Node) => void;
9+
unregister: (node: Node) => void;
10+
map: SvelteMap<Node, number | null>;
11+
elements: Array<HTMLElement | null>;
12+
labels?: Array<string | null>;
13+
};
14+
15+
const FloatingListContext = new Context<FloatingListContextType>(
16+
"FloatingListContext",
17+
);
18+
19+
interface UseListItemOptions {
20+
label?: MaybeGetter<string | null>;
21+
}
22+
23+
class ListItemState {
24+
#label = $derived.by(() => extract(this.opts.label));
25+
#listContext: FloatingListContextType;
26+
#index = $state<number | null>(null);
27+
#ref = $state<Node | null>(null);
28+
29+
constructor(private readonly opts: UseListItemOptions = {}) {
30+
this.#listContext = FloatingListContext.getOr({
31+
register: () => {},
32+
unregister: () => {},
33+
map: new SvelteMap(),
34+
elements: [],
35+
labels: [],
36+
});
37+
38+
$effect(() => {
39+
console.log("elements in listitemstate", this.#listContext.elements);
40+
console.log("element0 in listitemstate", this.#listContext.elements[0]);
41+
});
42+
43+
watch(
44+
() => this.#ref,
45+
() => {
46+
const node = this.#ref;
47+
if (node) {
48+
this.#listContext.register(node);
49+
return () => {
50+
this.#listContext.unregister(node);
51+
};
52+
}
53+
},
54+
);
55+
56+
$effect.pre(() => {
57+
const index = this.#ref ? this.#listContext.map.get(this.#ref) : null;
58+
if (index != null) {
59+
this.#index = index;
60+
}
61+
});
62+
}
63+
64+
get index() {
65+
return this.#index == null ? -1 : this.#index;
66+
}
67+
68+
get ref() {
69+
return this.#ref as HTMLElement | null;
70+
}
71+
72+
set ref(node: HTMLElement | null) {
73+
this.#ref = node;
74+
const idx = this.#index;
75+
const label = this.#label;
76+
77+
if (idx !== null) {
78+
this.#listContext.elements[idx] = node;
79+
this.#listContext.elements = this.#listContext.elements;
80+
if (this.#listContext.labels) {
81+
if (label !== undefined) {
82+
this.#listContext.labels[idx] = label;
83+
} else {
84+
this.#listContext.labels[idx] = node?.textContent ?? null;
85+
}
86+
}
87+
}
88+
}
89+
}
90+
91+
/**
92+
* Used to register a list item and its index (DOM position) in the
93+
* `FloatingList`.
94+
*/
95+
function useListItem(opts: UseListItemOptions = {}) {
96+
return new ListItemState(opts);
97+
}
98+
99+
/**
100+
* Used to register a list item and its index (DOM position) in the
101+
* `FloatingList`.
102+
*/
103+
104+
// function useListItem(opts: UseListItemOptions = {})
105+
106+
export { FloatingListContext, useListItem, ListItemState };
107+
export type { UseListItemOptions };

packages/floating-ui-svelte/src/hooks/use-dismiss.svelte.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,9 +330,7 @@ class DismissInteraction implements ElementProps {
330330
return;
331331
}
332332

333-
if (localInsideTree) {
334-
return;
335-
}
333+
if (localInsideTree) return;
336334

337335
if (
338336
typeof this.#outsidePress === "function" &&

packages/floating-ui-svelte/src/hooks/use-list-navigation.svelte.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,8 @@ class ListNavigationState {
316316
() => this.#rtl,
317317
() => this.#disabledIndices,
318318
],
319-
() => {
319+
(_, [__, prevOpen, prevFloating]) => {
320+
const prevMounted = !!prevFloating;
320321
if (!this.#enabled) return;
321322
if (!this.context.open) return;
322323
if (!this.context.floating) return;
@@ -329,20 +330,25 @@ class ListNavigationState {
329330
}
330331

331332
// Reset while the floating element was open (e.g. the list changed).
332-
if (this.#mounted) {
333+
if (prevMounted) {
333334
this.#index = -1;
334335
this.#focusItem();
335336
}
336337

337338
// Initial sync.
339+
console.log("focusItemOnOpen", this.#focusItemOnOpen);
338340
if (
339-
(!this.#previousOpen || !this.#mounted) &&
341+
(!prevOpen || !prevMounted) &&
340342
this.#focusItemOnOpen &&
341343
(this.#key != null ||
342-
(this.#focusItemOnOpen === true && this.#key == null))
344+
((this.#focusItemOnOpen === true ||
345+
this.#focusItemOnOpen === "auto") &&
346+
this.#key == null))
343347
) {
348+
console.log("running stuff");
344349
let runs = 0;
345350
const waitForListPopulated = () => {
351+
console.log("this list is null", this.#listRef[0] == null);
346352
if (this.#listRef[0] == null) {
347353
// Avoid letting the browser paint if possible on the first try,
348354
// otherwise use rAF. Don't try more than twice, since something

packages/floating-ui-svelte/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ export { default as FloatingOverlay } from "./components/floating-overlay.svelte
2323
export * from "./components/floating-delay-group.svelte";
2424
export { default as FloatingDelayGroup } from "./components/floating-delay-group.svelte";
2525

26+
export * from "./components/floating-list/floating-list.svelte";
27+
export { default as FloatingList } from "./components/floating-list/floating-list.svelte";
28+
2629
/**
2730
* Hooks
2831
*/

packages/floating-ui-svelte/src/internal/handle-guard-focus.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ import { sleep } from "./sleep.js";
1111
* restoring the attribute.
1212
*/
1313

14-
function handleGuardFocus(guard: HTMLElement | null) {
14+
function handleGuardFocus(
15+
guard: HTMLElement | null,
16+
focusOptions?: Parameters<HTMLElement["focus"]>[0],
17+
) {
1518
if (!guard) return;
1619
const ariaHidden = guard.getAttribute("aria-hidden");
1720
guard.removeAttribute("aria-hidden");
18-
guard.focus();
21+
guard.focus(focusOptions);
1922
sleep().then(() => {
2023
if (ariaHidden === null) {
2124
guard.setAttribute("aria-hidden", "");

0 commit comments

Comments
 (0)