Skip to content
Draft
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
76 changes: 76 additions & 0 deletions packages/interact/repro/state-looks.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>State looks</title>
<style>
.card {
width: 240px;
padding: 24px;
border: 1px solid #999;
}
</style>
</head>
<body>
<!--
Build first (`yarn workspace @wix/interact build`), then serve the repo root and open this page.
Hover the card: the hover state scales the card (its own look) and colors its title, an element
style of the same state. The button toggles `selected` from code: no interaction sets it, so
its look is a code state in `config.effects`.
-->
<interact-element data-interact-key="card">
<div class="card"><h2 class="title">Title</h2></div>
</interact-element>
<button id="toggle">Toggle "selected" from code</button>
<script type="module">
import { Interact, generate } from '../dist/es/web.js';

const config = {
effects: {
selected: {
key: 'card',
selector: '.card',
transition: { styleProperties: [{ name: 'background-color', value: 'gold' }] },
},
},
interactions: [
{
key: 'card',
trigger: 'hover',
selector: '.card',
effects: [
{
key: 'card',
selector: '.card',
effectId: 'hovered',
transition: {
styleProperties: [
{ name: 'transition', value: 'all 0.3s ease 0s' },
{ name: 'transform', value: 'scale(1.05)' },
],
},
elementStyles: [
{
selector: '.card .title',
styleProperties: [
{ name: 'transition', value: 'all 0.3s ease 0s' },
{ name: 'color', value: 'crimson' },
],
},
],
},
],
},
],
};

document.head.insertAdjacentHTML('beforeend', `<style>${generate(config)}</style>`);
Interact.create(config);

document.getElementById('toggle').addEventListener('click', () => {
document.querySelector('interact-element').toggleEffect('selected', 'toggle');
});
</script>
</body>
</html>
7 changes: 6 additions & 1 deletion packages/interact/src/core/Interact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
IInteractElement,
InteractPlugin,
} from '../types';
import { getElementHash, getInterpolatedKey } from './utilities';
import { getCodeStates, getElementHash, getInterpolatedKey } from './utilities';
import { getKeyframeSlots, getSlotKey } from './css';
import TRIGGER_TO_HANDLER_MODULE_MAP from '../handlers';
import {
Expand Down Expand Up @@ -391,6 +391,7 @@ function _ensureInteractionEntry(
sequences: {},
interactionIds: new Set(),
selectors: new Set(),
codeStates: [],
};
}

Expand Down Expand Up @@ -573,6 +574,10 @@ function parseConfig(config: InteractConfig, useCustomElement: boolean = false):
});
});

getCodeStates(config).forEach((effect) => {
_ensureInteractionEntry(interactions, effect.key).codeStates.push(effect);
});

return {
effects: effectMap,
sequences: sequenceMap,
Expand Down
33 changes: 32 additions & 1 deletion packages/interact/src/core/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,7 @@ function addInteraction<T extends TriggerType>(
effectId: (effect as Effect).effectId!,
transition: (effect as StateEffect).transition,
transitionProperties: (effect as StateEffect).transitionProperties,
elementStyles: (effect as StateEffect).elementStyles,
childSelector: getSelector(effect, {
asCombinator: true,
addItemFilter: true,
Expand Down Expand Up @@ -928,7 +929,37 @@ export function add(controller: IInteractionController): boolean {
hasEffects = addEffectsForTarget(key, controller, instance);
}

return hasTriggers || hasEffects;
const { codeStates = [] } = instance.get(key) || {};
codeStates.forEach((effect, index) => {
const { conditions = [] } = effect;
const mql = getMediaQuery(conditions, instance.dataCache.conditions);

if (mql) {
instance.setupMediaQueryListener(`${key}::codeState::${index}`, mql, key, () => {
controller.update();
});
}

if (!mql || mql.matches) {
controller.renderStyle(
createTransitionCSS({
key,
effectId: effect.effectId!,
transition: (effect as StateEffect).transition,
transitionProperties: (effect as StateEffect).transitionProperties,
elementStyles: (effect as StateEffect).elementStyles,
childSelector: getSelector(effect, {
asCombinator: true,
addItemFilter: true,
useFirstChild: controller.useFirstChild,
}),
selectorCondition: getSelectorCondition(conditions, instance.dataCache.conditions),
}),
);
}
});

return hasTriggers || hasEffects || codeStates.length > 0;
}

export function addListItems(
Expand Down
56 changes: 43 additions & 13 deletions packages/interact/src/core/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from '../utils';
import { getSelector } from './Interact';
import { resolveEffectForCSS, resolveSequenceForCSS } from './resolvers';
import { getElementHash } from './utilities';
import { getCodeStates, getElementHash } from './utilities';
import {
LIST_ANIMATION_PROPERTY_NAMES,
LIST_PROPERTY_NAMES,
Expand Down Expand Up @@ -385,15 +385,25 @@ function effectToCSS(
wrote.transition = true;
}

// adding state rule
rules.push({
key,
media,
selectorCondition,
childSelector,
states: [effectId],
declarations: properties,
});
// adding state rules: the target's, then the other elements' of the key
rules.push(
{
key,
media,
selectorCondition,
childSelector,
states: [effectId],
declarations: properties,
},
...(effect.elementStyles || []).map(({ selector, styleProperties }) => ({
key,
media,
selectorCondition,
childSelector: selector,
states: [effectId],
declarations: getStateStyleProperties({ transition: { styleProperties } }),
})),
);
} else {
// setting off animation custom properties
declarations.push(
Expand Down Expand Up @@ -619,13 +629,33 @@ function parseConfig(
plugins,
};

const cssRules = config.interactions.flatMap((interaction, interactionIdx) =>
parseInteraction(ctx, interaction, interactionIdx),
);
const cssRules = [
...config.interactions.flatMap((interaction, interactionIdx) =>
parseInteraction(ctx, interaction, interactionIdx),
),
...parseCodeStates(ctx),
];

return { ctx, cssRules };
}

// the looks of the states code toggles, each as a state effect of an interaction of its own
function parseCodeStates(ctx: GenerateContext): CSSRuleData[] {
const visited = new Set<string>();
const trigger = { id: 'code-states', componentId: '' } as TriggerVariant;
const rules = getCodeStates(ctx.config).flatMap((effect) => {
const resolved = resolveEffectForCSS(effect, { key: effect.key } as Interaction, {
...ctx.config,
effects: {},
});
return resolved ? parseEffect(ctx, resolved, trigger, visited) : [];
});

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

return rules;
}

export function getSlotKey(interactionIndex: number, effectIndex: number, sequenceIndex?: number) {
return [interactionIndex, sequenceIndex, effectIndex].filter((i) => i !== undefined).join(':');
}
Expand Down
41 changes: 40 additions & 1 deletion packages/interact/src/core/utilities.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import type { Interaction, ElementIdentifier, TimeAnimationTriggerType } from '../types';
import type {
Effect,
EffectRef,
ElementIdentifier,
InteractConfig,
Interaction,
StateEffect,
TimeAnimationTriggerType,
} from '../types';

export function _processKeysForInterpolation(key: string) {
return [...key.matchAll(/\[([-\w]+)]/g)].map(([_, _instanceKey]) => _instanceKey);
Expand Down Expand Up @@ -31,3 +39,34 @@ export function getElementHash(elementIdentifier: ElementIdentifier): string {
const { key, listContainer, listItemSelector, selector } = elementIdentifier;
return `${key}\0${listContainer || ''}\0${listItemSelector || ''}\0${selector || ''}`;
}

/**
* The state effects of `config.effects` that no interaction or sequence references: states that
* code toggles (`toggleEffect`), which still need their look. Each is named by its `effectId`, else
* its entry's id.
*/
export function getCodeStates(
config: InteractConfig,
): (Effect & { key: string; effectId: string })[] {
const { effects = {}, interactions = [], sequences = {} } = config;
const referenced = new Set<string>();
const addReferences = (list: (Effect | EffectRef)[] = []) =>
list.forEach(({ effectId }) => effectId && referenced.add(effectId));

Object.values(sequences).forEach((sequence) => addReferences(sequence.effects));
interactions.forEach((interaction) => {
addReferences(interaction.effects);
interaction.sequences?.forEach((sequence) => {
if ('effects' in sequence) {
addReferences(sequence.effects);
}
});
});

return Object.entries(effects).flatMap(([id, effect]) => {
const { transition, transitionProperties } = effect as StateEffect;
return !referenced.has(id) && effect.key && (transition || transitionProperties)
? [{ ...effect, key: effect.key, effectId: effect.effectId || id }]
: [];
});
}
9 changes: 8 additions & 1 deletion packages/interact/src/types/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type { TriggerType, TriggerParams } from './triggers';
import type { Effect, EffectRef, EffectProperty, TimeAnimationTriggerType } from './effects';
import type {
Effect,
EffectRef,
EffectProperty,
StateEffect,
TimeAnimationTriggerType,
} from './effects';
import type { PluginFields } from './plugins';

export type Condition = {
Expand Down Expand Up @@ -64,6 +70,7 @@ export type ResolvedEffect = ElementIdentifier &
conditions: string[];
triggerType: TimeAnimationTriggerType;
initial: boolean;
elementStyles?: StateEffect['elementStyles'];
suffix?: string;
// the effect's position in the config (see getSlotKey)
slotKey?: string;
Expand Down
10 changes: 10 additions & 0 deletions packages/interact/src/types/effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,20 @@ export type StyleProperty = {

export type TransitionProperty = StyleProperty & TransitionOptions;

/**
* Declarations a state sets on another element of its key, which `selector` finds the way an
* effect's `selector` does: inside the key's element.
*/
export type ElementStyle = {
selector: string;
styleProperties: StyleProperty[];
};

export type StateEffect = {
key?: string;
effectId?: string;
stateAction?: StateAction;
elementStyles?: ElementStyle[];
} & {
transition?: TransitionOptions & {
styleProperties: StyleProperty[];
Expand Down
1 change: 1 addition & 0 deletions packages/interact/src/types/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type {
TransitionOptions,
StyleProperty,
TransitionProperty,
ElementStyle,
StateEffect,
EffectRef,
Effect,
Expand Down
3 changes: 3 additions & 0 deletions packages/interact/src/types/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export type InteractCache = {
sequences: Record<string, (InteractionTrigger & { sequence: SequenceConfig })[]>;
interactionIds: Set<string>;
selectors: Set<string>;
// the key's states no interaction sets, for code to toggle
codeStates: Effect[];
};
};
};
Expand All @@ -27,6 +29,7 @@ export type CreateTransitionCSSParams = {
effectId: string;
transition?: StateEffect['transition'];
transitionProperties?: TransitionProperty[];
elementStyles?: StateEffect['elementStyles'];
childSelector?: string;
selectorCondition?: string;
useFirstChild?: boolean;
Expand Down
Loading
Loading