mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-10-21 23:01:26 +02:00
fix: tip returns to its row
This commit is contained in:
1 parent
f1bfe7d0e8
commit
0652a21bc3
3 files changed
+62
-3
No files matched your search
@@ -292,10 +292,21 @@ function watchMoves(element: Element, onMove: () => void): () => void {
|
||||
* that shows. The element followed last keeps the mark while it shows; when
|
||||
* it scrolls away or unmounts, the nearest one of the same id takes over.
|
||||
*/
|
||||
/**
|
||||
* What names an anchor element across remounts: its table row, when it sits
|
||||
* in one (a virtualized row comes back as a new element), else the element.
|
||||
*/
|
||||
function anchorKey(item: HTMLElement): string | HTMLElement {
|
||||
return (
|
||||
item.closest("[data-table-row]")?.getAttribute("data-table-row") ?? item
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAnchor(
|
||||
anchors: readonly CoachAnchorId[],
|
||||
lens: ReturnType<typeof createLens>,
|
||||
memory: Map<CoachAnchorId, { item: HTMLElement; box: Box }>,
|
||||
homes: Map<CoachAnchorId, string | HTMLElement>,
|
||||
): AnchorCandidate<HTMLElement> | null {
|
||||
const candidates: AnchorCandidate<HTMLElement>[] = [];
|
||||
for (const id of anchors) {
|
||||
@@ -310,8 +321,12 @@ function resolveAnchor(
|
||||
candidates,
|
||||
(candidate) => lens.shows(candidate.item, candidate.box),
|
||||
(id) => memory.get(id),
|
||||
(candidate) => homes.get(candidate.id) === anchorKey(candidate.item),
|
||||
);
|
||||
if (found) memory.set(found.id, { item: found.item, box: found.box });
|
||||
if (found) {
|
||||
memory.set(found.id, { item: found.item, box: found.box });
|
||||
if (!homes.has(found.id)) homes.set(found.id, anchorKey(found.item));
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
@@ -489,6 +504,8 @@ export function CoachMark({
|
||||
const memoryRef = useRef(
|
||||
new Map<CoachAnchorId, { item: HTMLElement; box: Box }>(),
|
||||
);
|
||||
/** The element each anchor id pointed at first, by its key. */
|
||||
const homesRef = useRef(new Map<CoachAnchorId, string | HTMLElement>());
|
||||
|
||||
const cardX = useMotionValue(0);
|
||||
const cardY = useMotionValue(0);
|
||||
@@ -545,7 +562,7 @@ export function CoachMark({
|
||||
const lens = createLens();
|
||||
const found = blocked
|
||||
? null
|
||||
: resolveAnchor(anchors, lens, memoryRef.current);
|
||||
: resolveAnchor(anchors, lens, memoryRef.current, homesRef.current);
|
||||
const element = found?.item ?? null;
|
||||
const hidden = blocked || (!found && !floatWithoutAnchor);
|
||||
const { viewport } = lens;
|
||||
|
||||
@@ -262,3 +262,40 @@ test("anchor ids are unique and map to their attribute", () => {
|
||||
assert.ok(isRailAnchor("rail-settings"));
|
||||
assert.ok(!isRailAnchor("column-dns"));
|
||||
});
|
||||
|
||||
test("the element a mark started on takes it back when it shows again", () => {
|
||||
const box = (top) => ({ x: 0, y: top, width: 20, height: 20 });
|
||||
const rows = [1, 2, 3].map((n) => ({
|
||||
id: "profile-info",
|
||||
item: `row-${n}`,
|
||||
box: box(n * 40),
|
||||
}));
|
||||
const shows = () => true;
|
||||
const followed = { item: "row-3", box: box(120) };
|
||||
assert.equal(
|
||||
pickAnchor(["profile-info"], rows, shows, () => followed).item,
|
||||
"row-3",
|
||||
"without a home the followed row keeps the mark",
|
||||
);
|
||||
assert.equal(
|
||||
pickAnchor(
|
||||
["profile-info"],
|
||||
rows,
|
||||
shows,
|
||||
() => followed,
|
||||
(candidate) => candidate.item === "row-1",
|
||||
).item,
|
||||
"row-1",
|
||||
);
|
||||
assert.equal(
|
||||
pickAnchor(
|
||||
["profile-info"],
|
||||
rows,
|
||||
(candidate) => candidate.item !== "row-1",
|
||||
() => followed,
|
||||
(candidate) => candidate.item === "row-1",
|
||||
).item,
|
||||
"row-3",
|
||||
"a home out of view does not take the mark",
|
||||
);
|
||||
});
|
||||
@@ -286,16 +286,21 @@ function distance(a: Box, b: Box): number {
|
||||
* every row), the one the mark followed last stays while it shows; when it
|
||||
* scrolls away or unmounts, the nearest one to where it was takes over, so
|
||||
* the mark moves a row and does not jump across the table. With nothing of
|
||||
* that id followed before, the first element in page order wins.
|
||||
* that id followed before, the first element in page order wins. The element
|
||||
* the mark started on (`isHome`) takes it back whenever it shows again, so a
|
||||
* scroll back to where the tip began puts the mark where it began.
|
||||
*/
|
||||
export function pickAnchor<T>(
|
||||
anchors: readonly CoachAnchorId[],
|
||||
candidates: readonly AnchorCandidate<T>[],
|
||||
shows: (candidate: AnchorCandidate<T>) => boolean,
|
||||
recall: (id: CoachAnchorId) => { item: T; box: Box } | undefined,
|
||||
isHome: (candidate: AnchorCandidate<T>) => boolean = () => false,
|
||||
): AnchorCandidate<T> | null {
|
||||
for (const id of anchors) {
|
||||
const own = candidates.filter((candidate) => candidate.id === id);
|
||||
const home = own.find((candidate) => isHome(candidate) && shows(candidate));
|
||||
if (home) return home;
|
||||
const last = recall(id);
|
||||
if (!last) {
|
||||
const first = own.find(shows);
|
||||
|
||||
Reference in new issue
Block a user