Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions packages/interact/src/core/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
buildAtPropertyRules,
getCustomPropName,
buildSequenceListsRule,
buildTimelineScopeRule,
LIST_KINDS,
listKind,
} from './cssUtils';
Expand Down Expand Up @@ -81,6 +82,8 @@ type GenerateContext = {
keyframeSlots: Map<string, string>;
// only collecting the effects' list entries for getKeyframeSlots, without generating CSS
collectSlots: boolean;
// timelines that animate elements outside their source, which need a timeline-scope to resolve
scopedTimelines: string[];
useFirstChild: boolean;
plugins?: InteractPluginStyles;
};
Expand Down Expand Up @@ -543,8 +546,9 @@ function parseInteraction(
componentId: '',
} as TriggerVariant;
const visited = new Set<string>();
const isViewProgress = trigger === 'viewProgress';

if (trigger === 'viewProgress') {
if (isViewProgress) {
cssRules.push(triggerToCSS(ctx, interaction, motionTrigger.id, visited));
}

Expand Down Expand Up @@ -582,6 +586,13 @@ function parseInteraction(
),
);

if (isViewProgress) {
const sourceHash = getElementHash(interaction);
if ([...visited].some((targetHash) => targetHash !== sourceHash)) {
ctx.scopedTimelines.push(`--${motionTrigger.id}`);
}
}

visited.forEach((targetHash) => endInteraction(ctx.targetsMap.get(targetHash)!));

return cssRules;
Expand Down Expand Up @@ -615,6 +626,7 @@ function parseConfig(
keyframesMap: new Map<string, Keyframe[]>(),
keyframeSlots: collectSlots ? new Map() : getKeyframeSlots(config),
collectSlots,
scopedTimelines: [],
useFirstChild,
plugins,
};
Expand Down Expand Up @@ -671,6 +683,7 @@ export function _generate(
): {
cssRules: CSSRuleData[];
listsRule: string;
timelineScopeRule: string;
keyframes: Map<string, Keyframe[]>;
atProperty: string[];
} {
Expand All @@ -696,7 +709,9 @@ export function _generate(
timelineLength,
);

return { keyframes: ctx.keyframesMap, atProperty, cssRules, listsRule };
const timelineScopeRule = buildTimelineScopeRule(ctx.scopedTimelines);

return { keyframes: ctx.keyframesMap, atProperty, cssRules, listsRule, timelineScopeRule };
}
/**
* Generates CSS for animations from an InteractConfig.
Expand All @@ -714,13 +729,17 @@ export function _generate(
* @returns string containing all of the CSS rules needed for time-based animations
*/
export function generate(config: InteractConfig, options?: boolean | GenerateOptions): string {
const { cssRules, keyframes, atProperty, listsRule } = _generate(config, options);
const { cssRules, keyframes, atProperty, listsRule, timelineScopeRule } = _generate(
config,
options,
);

const css = [
...atProperty,
...[...keyframes.entries()].map(([name, keyframes]) => keyframesToCSS(name, keyframes)),
...cssRules.map(CSSRuleToString),
listsRule,
timelineScopeRule,
].filter((rule) => rule);

return css.join('\n');
Expand Down
9 changes: 9 additions & 0 deletions packages/interact/src/core/cssUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,12 @@ export function buildListsRule(

return `${joinedSelector} {\n${declarations.map(({ name, value }) => ` ${name}: ${value};`).join('\n')}\n}`;
}

// a source's view-timeline only reaches its descendants, so its other targets need the name scoped on a common ancestor
export function buildTimelineScopeRule(timelineNames: string[]): string {
if (timelineNames.length === 0) {
return '';
}

return `:root {\n timeline-scope: ${timelineNames.join(', ')};\n}`;
}
31 changes: 31 additions & 0 deletions packages/interact/test/css.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1906,6 +1906,37 @@ describe('css._generate', () => {
expect(desktop).toMatchObject({ name: '--vw-tmln-1', value: '--trigger-1' });
expect(desktop.media).toContain('min-width: 1024px');
});

it('scopes the timelines that animate elements outside their source', () => {
const config: InteractConfig = {
interactions: [
{ key: 'a', trigger: 'viewProgress', effects: [scrub('self')] },
{ key: 'a', trigger: 'viewProgress', effects: [{ ...scrub('other'), key: 'b' }] },
{
key: 'a',
trigger: 'viewProgress',
sequences: [{ effects: [scrub('seq-self'), { ...scrub('seq-other'), key: 'c' }] }],
},
],
};

expect(_generate(config).timelineScopeRule).toBe(
':root {\n timeline-scope: --trigger-1, --trigger-2;\n}',
);
expect(generate(config)).toContain('timeline-scope: --trigger-1, --trigger-2;');
});

it('does not scope timelines that only animate their source', () => {
const config: InteractConfig = {
interactions: [
{ key: 'a', trigger: 'viewProgress', effects: [scrub('in')] },
{ key: 'b', trigger: 'viewProgress', effects: [scrub('other')] },
],
};

expect(_generate(config).timelineScopeRule).toBe('');
expect(generate(config)).not.toContain('timeline-scope');
});
});

describe('_generate endpoint edge cases', () => {
Expand Down
Loading