Skip to content
Open
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
5 changes: 3 additions & 2 deletions apps/demo/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Playground } from './components/Playground';
import { ScrollShowcase } from './components/ScrollShowcase';
import { ResponsiveDemo } from './components/ResponsiveDemo';

const heroCopy = [
'Tune triggers, easings, and delays in real time.',
'Preview viewProgress and hover behaviors without leaving the repo.',
'Copy the JSON config directly into CMS or product experiments.'
'Copy the JSON config directly into CMS or product experiments.',
];

function App() {
Expand All @@ -29,11 +30,11 @@ function App() {

<Playground />
<div className="scroll-showcase-wrapper">
<ResponsiveDemo />
<ScrollShowcase />
</div>
</div>
);
}

export default App;

234 changes: 234 additions & 0 deletions apps/demo/src/components/ResponsiveDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { InteractConfig } from '@wix/interact';
import { useEffect, useState } from 'react';
import { useInteractInstance } from '../hooks/useInteractInstance';

const complexConfig: InteractConfig = {
conditions: {
desktop: {
type: 'media',
predicate: 'min-width: 1024px',
},
mobile: {
type: 'media',
predicate: 'max-width: 767px',
},
tabletMinWidth: {
type: 'media',
predicate: 'min-width: 768px',
},
tabletMaxWidth: {
type: 'media',
predicate: 'max-width: 1025px',
},
},
interactions: [
{
trigger: 'click',
params: {
type: 'repeat',
},
key: 'multi-source-1',
effects: [
{
key: 'cascade-target-1',
effectId: 'desktop-effect',
conditions: ['desktop'],
},
{
key: 'cascade-target-2',
effectId: 'tablet-effect',
conditions: ['tabletMinWidth', 'tabletMaxWidth'],
},
{
key: 'cascade-target-3',
effectId: 'mobile-effect',
conditions: ['mobile'],
},
],
},
],
effects: {
'desktop-effect': {
//@ts-ignore
namedEffect: {
type: 'SlideIn',
},
duration: 1800,
},
'mobile-effect': {
namedEffect: {
type: 'BounceIn',
direction: 'center',
power: 'hard',
},
duration: 1800,
},
'tablet-effect': {
namedEffect: {
type: 'FlipIn',
direction: 'left',
},
duration: 1800,
},
},
};

export const ResponsiveDemo = () => {
const [width, setWidth] = useState(window.innerWidth);

useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);

useInteractInstance(complexConfig);

return (
<section className="panel" style={{ marginTop: '20px', position: 'relative' }}>
<p className="scroll-label">Responsive Demo</p>
<div
style={{
position: 'absolute',
top: '10px',
right: '10px',
fontSize: '48px',
color: '#faf7f7',
}}
>
screen width: {width}px
</div>
<div style={{ padding: '20px' }}>
<h3 style={{ marginBottom: '10px' }}>Responsive Interactions</h3>
<p style={{ marginBottom: '20px', color: '#888' }}>
Resize the window to see different effects on click.
<br />
<strong>Desktop (≥1024px):</strong> SlideIn on Target 1<br />
<strong>Tablet (≥767 and ≤1025px):</strong> FlipIn on Target 2<br />
<strong>Mobile (≤767px):</strong> BounceIn on Target 3
</p>

<div style={{ marginBottom: '30px' }}>
{/* Trigger */}
<interact-element data-interact-key="multi-source-1" style={{ display: 'inline-block' }}>
<button
style={{
padding: '12px 24px',
fontSize: '16px',
cursor: 'pointer',
background: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '6px',
fontWeight: 'bold',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
}}
>
Trigger Animation
</button>
</interact-element>
</div>

<div style={{ display: 'flex', gap: '20px', flexWrap: 'wrap' }}>
{/* Target 1 */}
<div style={{ textAlign: 'center' }}>
<p
style={{
marginBottom: '8px',
fontSize: '12px',
textTransform: 'uppercase',
letterSpacing: '1px',
}}
>
Desktop Target
</p>
<interact-element data-interact-key="cascade-target-1">
<div
style={{
width: '150px',
height: '150px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '12px',
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
fontSize: '14px',
fontWeight: 'bold',
}}
>
Slide In
</div>
</interact-element>
</div>

{/* Target 2 */}
<div style={{ textAlign: 'center' }}>
<p
style={{
marginBottom: '8px',
fontSize: '12px',
textTransform: 'uppercase',
letterSpacing: '1px',
}}
>
Tablet Target
</p>
<interact-element data-interact-key="cascade-target-2">
<div
style={{
width: '150px',
height: '150px',
background: 'linear-gradient(135deg, #2d1ccc 0%, #e1e1e1 99%, #500036 100%)',
color: '#333',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '12px',
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
fontSize: '14px',
fontWeight: 'bold',
}}
>
Flip In
</div>
</interact-element>
</div>
{/* Target 3 */}
<div style={{ textAlign: 'center' }}>
<p
style={{
marginBottom: '8px',
fontSize: '12px',
textTransform: 'uppercase',
letterSpacing: '1px',
}}
>
Mobile Target
</p>
<interact-element data-interact-key="cascade-target-3">
<div
style={{
width: '150px',
height: '150px',
background: 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 99%, #fecfef 100%)',
color: '#333',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '12px',
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
fontSize: '14px',
fontWeight: 'bold',
}}
>
Bounce In
</div>
</interact-element>
</div>
</div>
</div>
</section>
);
};
1 change: 0 additions & 1 deletion apps/demo/src/hooks/useInteractInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,3 @@ export const useInteractInstance = (config: InteractConfig) => {
};
}, [config]);
};

27 changes: 10 additions & 17 deletions packages/interact/src/InteractElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ export function getInteractElement() {
disconnect() {
const key = this.dataset.interactKey;

if (key) {
// Only call remove() if the element is actually being removed from DOM
// If the element is still connected to the DOM (this.isConnected === true),
// we're just disconnecting due to instance destruction (e.g., React StrictMode),
// so we should keep the element in the cache for reconnection
if (key && !this.isConnected) {
remove(key);
}

Expand Down Expand Up @@ -101,11 +105,7 @@ export function getInteractElement() {
}
}

toggleEffect(
effectId: string,
method: StateParams['method'],
item?: HTMLElement | null,
) {
toggleEffect(effectId: string, method: StateParams['method'], item?: HTMLElement | null) {
if (item === null) {
return;
}
Expand All @@ -126,9 +126,7 @@ export function getInteractElement() {
this._internals.states.clear();
}
} else {
const currentEffects = new Set(
this.dataset[INTERACT_EFFECT_DATA_ATTR]?.split(' ') || [],
);
const currentEffects = new Set(this.dataset[INTERACT_EFFECT_DATA_ATTR]?.split(' ') || []);

if (method === 'toggle') {
currentEffects.has(effectId)
Expand All @@ -142,17 +140,14 @@ export function getInteractElement() {
currentEffects.clear();
}

(item || this).dataset[INTERACT_EFFECT_DATA_ATTR] =
Array.from(currentEffects).join(' ');
(item || this).dataset[INTERACT_EFFECT_DATA_ATTR] = Array.from(currentEffects).join(' ');
}
}

getActiveEffects(): string[] {
if (this._internals) {
const effects = Array.from(this._internals.states);
return isLegacyStateSyntax
? effects.map((effect) => effect.replace(/^--/g, ''))
: effects;
return isLegacyStateSyntax ? effects.map((effect) => effect.replace(/^--/g, '')) : effects;
}

const raw = this.dataset[INTERACT_EFFECT_DATA_ATTR] || '';
Expand All @@ -168,9 +163,7 @@ export function getInteractElement() {
let observer = this._observers.get(list as HTMLElement);

if (!observer) {
observer = new MutationObserver(
this._childListChangeHandler.bind(this, listContainer),
);
observer = new MutationObserver(this._childListChangeHandler.bind(this, listContainer));

this._observers.set(list as HTMLElement, observer);

Expand Down
Loading