Skip to content

Commit 3d7ba4e

Browse files
committed
chore: add exit animation options to Animate On Scroll block
- Introduced new attributes for exit animation type and direction, allowing users to customize how elements exit the viewport. - Updated the edit component to include controls for selecting exit animations and directions, enhancing user experience.
1 parent 97005c4 commit 3d7ba4e

File tree

5 files changed

+247
-23
lines changed

5 files changed

+247
-23
lines changed

src/blocks-interactivity/animate-on-scroll/block.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@
8989
"type": "boolean",
9090
"default": false
9191
},
92+
"exitAnimation": {
93+
"type": "string",
94+
"default": "fade"
95+
},
96+
"exitDirection": {
97+
"type": "string",
98+
"default": "up"
99+
},
92100
"useSequence": {
93101
"type": "boolean",
94102
"default": false

src/blocks-interactivity/animate-on-scroll/edit.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ type BlockAttributes = {
5454
elasticDistance: number;
5555
initialDelay: number;
5656
reAnimateOnScroll: boolean;
57+
exitAnimation: string;
58+
exitDirection: string;
5759
useSequence: boolean;
5860
animationSequence: Array<{
5961
animation: string;
@@ -919,6 +921,58 @@ export default function Edit({ attributes, setAttributes }: EditProps) {
919921
)}
920922
__nextHasNoMarginBottom
921923
/>
924+
925+
{attributes.reAnimateOnScroll && (
926+
<>
927+
<SelectControl
928+
label={__('Exit Animation Type', 'aggressive-apparel')}
929+
value={attributes.exitAnimation || 'fade'}
930+
options={Object.entries(baseAnimations).map(
931+
([value, config]) => ({
932+
value,
933+
label: config.label,
934+
})
935+
)}
936+
onChange={exitAnimation => {
937+
const animationKey = exitAnimation as AnimationKey;
938+
const config = baseAnimations[animationKey];
939+
const newDirection = config.hasDirection
940+
? (config as { defaultDirection: string }).defaultDirection
941+
: '';
942+
setAttributes({
943+
exitAnimation,
944+
exitDirection: newDirection,
945+
});
946+
}}
947+
__next40pxDefaultSize
948+
/>
949+
{baseAnimations[attributes.exitAnimation as AnimationKey]
950+
?.hasDirection && (
951+
<SelectControl
952+
label={__('Exit Direction', 'aggressive-apparel')}
953+
value={attributes.exitDirection || 'up'}
954+
options={
955+
(
956+
baseAnimations[
957+
attributes.exitAnimation as AnimationKey
958+
] as {
959+
directions: Array<{ label: string; value: string }>;
960+
}
961+
)?.directions || []
962+
}
963+
onChange={exitDirection => {
964+
setAttributes({ exitDirection });
965+
}}
966+
help={__(
967+
'Direction for exit animation when scrolling up',
968+
'aggressive-apparel'
969+
)}
970+
__next40pxDefaultSize
971+
__nextHasNoMarginBottom
972+
/>
973+
)}
974+
</>
975+
)}
922976
</PanelBody>
923977

924978
{/* Debug Panel */}

src/blocks-interactivity/animate-on-scroll/render.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@
3434
in_array( $attributes['animation'], array( 'slide', 'flip', 'rotate', 'zoom', 'bounce' ), true ) ) {
3535
$default_classes[] = esc_attr( $attributes['direction'] );
3636
}
37+
38+
// Add exit animation classes if re-animate is enabled.
39+
if ( ! empty( $attributes['reAnimateOnScroll'] ) && ! empty( $attributes['exitAnimation'] ) ) {
40+
$exit_animation_class = 'blur' === $attributes['exitAnimation'] ? 'blur-in' : esc_attr( $attributes['exitAnimation'] );
41+
$default_classes[] = 'exit-' . $exit_animation_class;
42+
43+
// Add exit direction for animations that support it.
44+
if ( ! empty( $attributes['exitDirection'] ) &&
45+
in_array( $attributes['exitAnimation'], array( 'slide', 'flip', 'rotate', 'zoom', 'bounce' ), true ) ) {
46+
$default_classes[] = 'exit-' . esc_attr( $attributes['exitDirection'] );
47+
}
48+
}
3749
}
3850

3951
// Join classes with spaces.

src/blocks-interactivity/animate-on-scroll/style.css

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,116 @@
469469
var(--wp-block-animate-on-scroll-animation-timing)
470470
forwards;
471471
}
472+
473+
/* Exit Animations - Apply when scrolling up and element is exiting */
474+
&.is-exiting:not(.has-animation-sequence) > * {
475+
opacity: 0;
476+
}
477+
478+
/* Exit Animation Type: Fade */
479+
&.exit-fade.is-exiting > * {
480+
opacity: 0;
481+
}
482+
483+
/* Exit Animation Type: Blur */
484+
&.exit-blur-in.is-exiting > * {
485+
filter: blur(var(--wp-block-animate-on-scroll-blur-amount));
486+
opacity: 0;
487+
}
488+
489+
/* Exit Animation Type: Slide */
490+
&.exit-slide {
491+
492+
&.exit-up.is-exiting > * {
493+
transform: translateY(var(--wp-block-animate-on-scroll-slide-distance));
494+
opacity: 0;
495+
}
496+
497+
&.exit-down.is-exiting > * {
498+
transform: translateY(calc(-1 * var(--wp-block-animate-on-scroll-slide-distance)));
499+
opacity: 0;
500+
}
501+
502+
&.exit-left.is-exiting > * {
503+
transform: translateX(var(--wp-block-animate-on-scroll-slide-distance));
504+
opacity: 0;
505+
}
506+
507+
&.exit-right.is-exiting > * {
508+
transform: translateX(calc(-1 * var(--wp-block-animate-on-scroll-slide-distance)));
509+
opacity: 0;
510+
}
511+
}
512+
513+
/* Exit Animation Type: Zoom */
514+
&.exit-zoom {
515+
516+
&.exit-in.is-exiting > * {
517+
transform-origin: center center;
518+
transform: scale(var(--wp-block-animate-on-scroll-zoom-in-start));
519+
opacity: 0;
520+
}
521+
522+
&.exit-out.is-exiting > * {
523+
transform-origin: center center;
524+
transform: scale(var(--wp-block-animate-on-scroll-zoom-out-start));
525+
opacity: 0;
526+
}
527+
}
528+
529+
/* Exit Animation Type: Flip */
530+
&.exit-flip {
531+
532+
&.exit-up.is-exiting > * {
533+
transform-origin: center center;
534+
transform: perspective(var(--wp-block-animate-on-scroll-perspective))
535+
rotateX(var(--wp-block-animate-on-scroll-rotate-angle));
536+
opacity: 0;
537+
}
538+
539+
&.exit-down.is-exiting > * {
540+
transform-origin: center center;
541+
transform: perspective(var(--wp-block-animate-on-scroll-perspective))
542+
rotateX(calc(-1 * var(--wp-block-animate-on-scroll-rotate-angle)));
543+
opacity: 0;
544+
}
545+
546+
&.exit-left.is-exiting > * {
547+
transform-origin: center center;
548+
transform: perspective(var(--wp-block-animate-on-scroll-perspective))
549+
rotateY(var(--wp-block-animate-on-scroll-rotate-angle));
550+
opacity: 0;
551+
}
552+
553+
&.exit-right.is-exiting > * {
554+
transform-origin: center center;
555+
transform: perspective(var(--wp-block-animate-on-scroll-perspective))
556+
rotateY(calc(-1 * var(--wp-block-animate-on-scroll-rotate-angle)));
557+
opacity: 0;
558+
}
559+
}
560+
561+
/* Exit Animation Type: Rotate */
562+
&.exit-rotate {
563+
564+
&.exit-left.is-exiting > * {
565+
transform-origin: center center;
566+
transform: rotate(var(--wp-block-animate-on-scroll-rotate-angle));
567+
opacity: 0;
568+
}
569+
570+
&.exit-right.is-exiting > * {
571+
transform-origin: center center;
572+
transform: rotate(calc(-1 * var(--wp-block-animate-on-scroll-rotate-angle)));
573+
opacity: 0;
574+
}
575+
}
576+
577+
/* Exit Animation Type: Bounce */
578+
&.exit-bounce.is-exiting > * {
579+
transform: translateY(var(--wp-block-animate-on-scroll-bounce-distance));
580+
opacity: 0;
581+
}
472582
}
473583

474584
/**

src/blocks-interactivity/animate-on-scroll/view.ts

Lines changed: 63 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ const { state, actions } = store('aggressive-apparel/animate-on-scroll', {
107107
isVisible: false,
108108
elementRef: null as HTMLElement | null,
109109
intersectionRatio: 0,
110+
previousRatio: 0,
110111
entryHeight: 0,
111112
ctx: {} as AnimateOnScrollContext,
112113
resizeTimeout: null as number | null,
@@ -495,6 +496,11 @@ const { state, actions } = store('aggressive-apparel/animate-on-scroll', {
495496
}
496497
}
497498

499+
// Track scroll direction by comparing current and previous intersection ratio
500+
const previousRatio = state.previousRatio;
501+
const isScrollingDown = entry.intersectionRatio > previousRatio;
502+
state.previousRatio = entry.intersectionRatio;
503+
498504
// Update intersection ratio
499505
ctx.intersectionRatio = entry.intersectionRatio;
500506

@@ -506,28 +512,35 @@ const { state, actions } = store('aggressive-apparel/animate-on-scroll', {
506512

507513
// When element crosses the visibility trigger (e.g., 50% visible)
508514
if (entry.intersectionRatio >= visibilityThreshold) {
509-
// Element is now visible - animate in
510-
if (!ctx.isVisible) {
511-
ctx.isVisible = true;
515+
// Element is now visible - animate in (only when scrolling down or already visible)
516+
if (!ctx.isVisible || (isScrollingDown && ctx.isVisible)) {
517+
// Remove exit animation class if present
518+
if (ref && ctx.reAnimateOnScroll) {
519+
ref.classList.remove('is-exiting');
520+
}
512521

513-
// Announce to screen readers (accessibility)
514-
if (ref) {
515-
const announcement = document.createElement('div');
516-
announcement.setAttribute('role', 'status');
517-
announcement.setAttribute('aria-live', 'polite');
518-
announcement.setAttribute('aria-atomic', 'true');
519-
announcement.className = 'screen-reader-text';
520-
announcement.style.cssText =
521-
'position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden;';
522-
announcement.textContent = 'Content animated into view';
523-
document.body.appendChild(announcement);
524-
525-
// Remove announcement after a delay
526-
setTimeout(() => {
527-
if (document.body.contains(announcement)) {
528-
document.body.removeChild(announcement);
529-
}
530-
}, 1000);
522+
if (!ctx.isVisible) {
523+
ctx.isVisible = true;
524+
525+
// Announce to screen readers (accessibility)
526+
if (ref) {
527+
const announcement = document.createElement('div');
528+
announcement.setAttribute('role', 'status');
529+
announcement.setAttribute('aria-live', 'polite');
530+
announcement.setAttribute('aria-atomic', 'true');
531+
announcement.className = 'screen-reader-text';
532+
announcement.style.cssText =
533+
'position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden;';
534+
announcement.textContent = 'Content animated into view';
535+
document.body.appendChild(announcement);
536+
537+
// Remove announcement after a delay
538+
setTimeout(() => {
539+
if (document.body.contains(announcement)) {
540+
document.body.removeChild(announcement);
541+
}
542+
}, 1000);
543+
}
531544
}
532545
}
533546

@@ -536,8 +549,35 @@ const { state, actions } = store('aggressive-apparel/animate-on-scroll', {
536549
observer.disconnect();
537550
}
538551
} else if (ctx.reAnimateOnScroll && ctx.isVisible) {
539-
// Element is now invisible - animate out (only if re-animation is enabled)
540-
ctx.isVisible = false;
552+
// Element is now invisible - apply exit animation when scrolling up
553+
if (!isScrollingDown && ref) {
554+
// Add exit animation class
555+
ref.classList.add('is-exiting');
556+
557+
// Get animation duration from CSS variable
558+
const durationValue = window
559+
.getComputedStyle(ref)
560+
.getPropertyValue(
561+
'--wp-block-animate-on-scroll-animation-duration'
562+
);
563+
const durationSeconds = durationValue
564+
? parseFloat(durationValue.replace('s', ''))
565+
: 0.5;
566+
567+
// After exit animation completes, hide element
568+
setTimeout(() => {
569+
ctx.isVisible = false;
570+
if (ref) {
571+
ref.classList.remove('is-exiting');
572+
}
573+
}, durationSeconds * 1000);
574+
} else {
575+
// Scrolling down past threshold - just hide immediately
576+
ctx.isVisible = false;
577+
if (ref) {
578+
ref.classList.remove('is-exiting');
579+
}
580+
}
541581
}
542582
});
543583
},

0 commit comments

Comments
 (0)