@@ -87,6 +87,44 @@ const OnInViewChangedComponentWithoutClenaup = ({
87
87
) ;
88
88
} ;
89
89
90
+ const ThresholdTriggerComponent = ( {
91
+ options,
92
+ } : {
93
+ options ?: IntersectionEffectOptions ;
94
+ } ) => {
95
+ const [ triggerCount , setTriggerCount ] = React . useState ( 0 ) ;
96
+ const [ lastRatio , setLastRatio ] = React . useState < number | null > ( null ) ;
97
+ const [ triggeredThresholds , setTriggeredThresholds ] = React . useState <
98
+ number [ ]
99
+ > ( [ ] ) ;
100
+
101
+ const inViewRef = useOnInView ( ( entry ) => {
102
+ setTriggerCount ( ( prev ) => prev + 1 ) ;
103
+ setLastRatio ( entry . intersectionRatio ) ;
104
+
105
+ // Add this ratio to our list of triggered thresholds
106
+ setTriggeredThresholds ( ( prev ) => [ ...prev , entry . intersectionRatio ] ) ;
107
+
108
+ return ( exitEntry ) => {
109
+ if ( exitEntry ) {
110
+ setLastRatio ( exitEntry . intersectionRatio ) ;
111
+ }
112
+ } ;
113
+ } , options ) ;
114
+
115
+ return (
116
+ < div
117
+ data-testid = "threshold-trigger"
118
+ ref = { inViewRef }
119
+ data-trigger-count = { triggerCount }
120
+ data-last-ratio = { lastRatio !== null ? lastRatio . toFixed ( 2 ) : "null" }
121
+ data-triggered-thresholds = { JSON . stringify ( triggeredThresholds ) }
122
+ >
123
+ Tracking thresholds
124
+ </ div >
125
+ ) ;
126
+ } ;
127
+
90
128
test ( "should create a hook with useOnInView" , ( ) => {
91
129
const { getByTestId } = render ( < OnInViewChangedComponent /> ) ;
92
130
const wrapper = getByTestId ( "wrapper" ) ;
@@ -172,7 +210,7 @@ test("should call callback with trigger: leave and triggerOnce is true", () => {
172
210
const wrapper = getByTestId ( "wrapper" ) ;
173
211
174
212
mockAllIsIntersecting ( true ) ;
175
- // initialInView should have triggered the callback once
213
+ // the callback should not be called as it is triggered on leave
176
214
expect ( wrapper . getAttribute ( "data-call-count" ) ) . toBe ( "0" ) ;
177
215
178
216
mockAllIsIntersecting ( false ) ;
@@ -338,7 +376,7 @@ test("should pass the element to the callback", () => {
338
376
339
377
const ElementTestComponent = ( ) => {
340
378
const inViewRef = useOnInView ( ( entry ) => {
341
- capturedElement = entry ? .target ;
379
+ capturedElement = entry . target ;
342
380
return undefined ;
343
381
} ) ;
344
382
@@ -351,3 +389,87 @@ test("should pass the element to the callback", () => {
351
389
352
390
expect ( capturedElement ) . toBe ( element ) ;
353
391
} ) ;
392
+
393
+ test ( "should track which threshold triggered the visibility change" , ( ) => {
394
+ // Using multiple specific thresholds
395
+ const { getByTestId } = render (
396
+ < ThresholdTriggerComponent options = { { threshold : [ 0.25 , 0.5 , 0.75 ] } } /> ,
397
+ ) ;
398
+ const element = getByTestId ( "threshold-trigger" ) ;
399
+
400
+ // Initially not in view
401
+ expect ( element . getAttribute ( "data-trigger-count" ) ) . toBe ( "0" ) ;
402
+
403
+ // Trigger at exactly the first threshold (0.25)
404
+ mockAllIsIntersecting ( 0.25 ) ;
405
+ expect ( element . getAttribute ( "data-trigger-count" ) ) . toBe ( "1" ) ;
406
+ expect ( element . getAttribute ( "data-last-ratio" ) ) . toBe ( "0.25" ) ;
407
+
408
+ // Go out of view
409
+ mockAllIsIntersecting ( 0 ) ;
410
+
411
+ // Trigger at exactly the second threshold (0.5)
412
+ mockAllIsIntersecting ( 0.5 ) ;
413
+ expect ( element . getAttribute ( "data-trigger-count" ) ) . toBe ( "2" ) ;
414
+ expect ( element . getAttribute ( "data-last-ratio" ) ) . toBe ( "0.50" ) ;
415
+
416
+ // Go out of view
417
+ mockAllIsIntersecting ( 0 ) ;
418
+
419
+ // Trigger at exactly the third threshold (0.75)
420
+ mockAllIsIntersecting ( 0.75 ) ;
421
+ expect ( element . getAttribute ( "data-trigger-count" ) ) . toBe ( "3" ) ;
422
+ expect ( element . getAttribute ( "data-last-ratio" ) ) . toBe ( "0.75" ) ;
423
+
424
+ // Check all triggered thresholds were recorded
425
+ const triggeredThresholds = JSON . parse (
426
+ element . getAttribute ( "data-triggered-thresholds" ) || "[]" ,
427
+ ) ;
428
+ expect ( triggeredThresholds ) . toContain ( 0.25 ) ;
429
+ expect ( triggeredThresholds ) . toContain ( 0.5 ) ;
430
+ expect ( triggeredThresholds ) . toContain ( 0.75 ) ;
431
+ } ) ;
432
+
433
+ test ( "should track thresholds when crossing multiple in a single update" , ( ) => {
434
+ // Using multiple specific thresholds
435
+ const { getByTestId } = render (
436
+ < ThresholdTriggerComponent options = { { threshold : [ 0.2 , 0.4 , 0.6 , 0.8 ] } } /> ,
437
+ ) ;
438
+ const element = getByTestId ( "threshold-trigger" ) ;
439
+
440
+ // Initially not in view
441
+ expect ( element . getAttribute ( "data-trigger-count" ) ) . toBe ( "0" ) ;
442
+
443
+ // Jump straight to 0.7 (crosses 0.2, 0.4, 0.6 thresholds)
444
+ // The IntersectionObserver will still only call the callback once
445
+ // with the highest threshold that was crossed
446
+ mockAllIsIntersecting ( 0.7 ) ;
447
+ expect ( element . getAttribute ( "data-trigger-count" ) ) . toBe ( "1" ) ;
448
+ expect ( element . getAttribute ( "data-last-ratio" ) ) . toBe ( "0.60" ) ;
449
+
450
+ // Go out of view
451
+ mockAllIsIntersecting ( 0 ) ;
452
+
453
+ // Jump to full visibility
454
+ mockAllIsIntersecting ( 1.0 ) ;
455
+ expect ( element . getAttribute ( "data-trigger-count" ) ) . toBe ( "2" ) ;
456
+ expect ( element . getAttribute ( "data-last-ratio" ) ) . toBe ( "0.80" ) ;
457
+ } ) ;
458
+
459
+ test ( "should track thresholds when trigger is set to leave" , ( ) => {
460
+ // Using multiple specific thresholds with trigger: leave
461
+ const { getByTestId } = render (
462
+ < ThresholdTriggerComponent
463
+ options = { {
464
+ threshold : [ 0.25 , 0.5 , 0.75 ] ,
465
+ trigger : "leave" ,
466
+ } }
467
+ /> ,
468
+ ) ;
469
+ const element = getByTestId ( "threshold-trigger" ) ;
470
+
471
+ // Make element 30% visible - above first threshold, should call cleanup
472
+ mockAllIsIntersecting ( 0 ) ;
473
+ expect ( element . getAttribute ( "data-trigger-count" ) ) . toBe ( "1" ) ;
474
+ expect ( element . getAttribute ( "data-last-ratio" ) ) . toBe ( "0.00" ) ;
475
+ } ) ;
0 commit comments