-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathfouc.ts
More file actions
74 lines (69 loc) · 3.22 KB
/
Copy pathfouc.ts
File metadata and controls
74 lines (69 loc) · 3.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import type { Path, SemanticIssue, AnyEffect, AnySequence, AnyInteraction } from '../types';
import { RETRIGGER_TYPES } from '../types';
// Mirrors the documented Element-Resolution priority (full-lean.md "Element
// Resolution"): the effect targets the source element when it does NOT introduce
// its own distinct target — `effect.key` is absent or equal to the interaction's
// `key`, and the effect adds no `selector` / `listContainer` / `listItemSelector`
// that the source doesn't already have. When in doubt, returns `false` to avoid
// false positives (e.g. registry effects with no owning interaction).
const DISCRETE_TRIGGERS = ['hover', 'click', 'interest', 'activate'];
const HIT_AREA_TRANSFORM = /(translate|scale|matrix)/;
export function targetsSameElementAsSource(owner: AnyInteraction, effect: AnyEffect): boolean {
if (effect.key !== undefined && effect.key !== owner.key) return false;
const refiners = ['selector', 'listContainer', 'listItemSelector'] as const;
for (const field of refiners) {
if ((effect[field] || owner[field]) && effect[field] !== owner[field]) return false;
}
return true;
}
// viewEnter same source+target with a re-triggering type (effect or sequence)
export function checkSameElementRetrigger(
path: Path,
item: AnyEffect | AnySequence,
owner?: AnyInteraction,
): SemanticIssue[] {
if (!owner || owner.trigger !== 'viewEnter') return [];
if (!item.triggerType || !RETRIGGER_TYPES.includes(item.triggerType)) return [];
const items = (item as AnySequence).effects || [item];
if (items.some((i: AnyEffect) => targetsSameElementAsSource(owner, i))) {
return [
{
code: 'custom',
params: { domainCode: 'SAME_ELEMENT_RETRIGGER' },
path: [...path, 'triggerType'],
message: `viewEnter with triggerType '${item.triggerType}' must use a separate source/target element; same-element observation causes re-trigger loops. Use a different \`key\`/\`selector\` or \`triggerType: 'once'\`.`,
},
];
}
return [];
}
// hover/pointerMove same source+target with size/position transforms
export function checkHitAreaShift(
path: Path,
effect: AnyEffect,
owner?: AnyInteraction,
): SemanticIssue[] {
if (!owner) return [];
const isDiscrete = DISCRETE_TRIGGERS.includes(owner.trigger ?? '');
const isPointer = owner.trigger === 'pointerMove';
if (!isDiscrete && !isPointer) return [];
// `hitArea: 'root'` tracks the viewport, so a transform on the source cannot
// shift the hit area. Only explicit `'self'` are at risk.
if (isPointer && owner.params?.hitArea !== 'self') return [];
if (!targetsSameElementAsSource(owner, effect)) return [];
const keyframes = effect.keyframeEffect?.keyframes;
if (!Array.isArray(keyframes)) return [];
const shifts = keyframes.some(
(frame) =>
typeof frame?.transform === 'string' && HIT_AREA_TRANSFORM.test(frame.transform as string),
);
if (!shifts) return [];
return [
{
code: 'custom',
params: { domainCode: 'HIT_AREA_SHIFT' },
path: [...path],
message: `${owner.trigger} effect changes size/position (transform) on the same element used as the source; the shifting hit area causes jittery re-entry. Target a child via \`selector\` or set a different \`key\`.`,
},
];
}