ripple 0.2.168 → 0.2.169
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/compiler/phases/2-analyze/index.js +4 -5
- package/src/compiler/phases/3-transform/client/index.js +20 -36
- package/src/compiler/utils.js +13 -6
- package/src/jsx-runtime.d.ts +5 -3
- package/src/runtime/internal/client/events.js +134 -25
- package/src/runtime/internal/client/index.js +1 -1
- package/src/runtime/internal/client/render.js +36 -26
- package/src/runtime/media-query.js +14 -8
- package/src/utils/events.js +120 -43
- package/tests/client/basic/basic.attributes.test.ripple +442 -0
- package/tests/client/basic/basic.events.test.ripple +2 -2
- package/tests/client/composite/composite.reactivity.test.ripple +6 -5
- package/tests/client/input-value.test.ripple +4 -4
- package/tests/client/media-query.test.ripple +1 -1
- package/tests/utils/events.test.js +57 -46
- package/types/index.d.ts +15 -5
package/src/utils/events.js
CHANGED
|
@@ -1,37 +1,67 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
/** @import { AddEventObject } from '#public'*/
|
|
2
|
+
|
|
3
|
+
const NON_DELEGATED_EVENTS = new Set([
|
|
4
|
+
'abort',
|
|
5
|
+
'afterprint',
|
|
6
|
+
'beforeprint',
|
|
7
|
+
'beforetoggle',
|
|
8
|
+
'beforeunload',
|
|
9
|
+
'blur',
|
|
10
|
+
'close',
|
|
11
|
+
'command',
|
|
12
|
+
'contextmenu',
|
|
13
|
+
'cuechange',
|
|
14
|
+
'DOMContentLoaded',
|
|
15
|
+
'error',
|
|
16
|
+
'focus',
|
|
17
|
+
'invalid',
|
|
18
|
+
'load',
|
|
19
|
+
'loadend',
|
|
20
|
+
'loadstart',
|
|
21
|
+
'mouseenter',
|
|
22
|
+
'mouseleave',
|
|
23
|
+
'pointerenter',
|
|
24
|
+
'pointerleave',
|
|
25
|
+
'progress',
|
|
26
|
+
'readystatechange',
|
|
27
|
+
'resize',
|
|
28
|
+
'scroll',
|
|
29
|
+
'scrollend',
|
|
30
|
+
'toggle',
|
|
31
|
+
'unload',
|
|
32
|
+
'visibilitychange',
|
|
33
|
+
// Media Events
|
|
34
|
+
'canplay',
|
|
35
|
+
'canplaythrough',
|
|
36
|
+
'durationchange',
|
|
37
|
+
'emptied',
|
|
38
|
+
'encrypted',
|
|
39
|
+
'ended',
|
|
40
|
+
'loadeddata',
|
|
41
|
+
'loadedmetadata',
|
|
42
|
+
'loadstart',
|
|
43
|
+
'pause',
|
|
44
|
+
'play',
|
|
45
|
+
'playing',
|
|
46
|
+
'progress',
|
|
47
|
+
'ratechange',
|
|
48
|
+
'seeked',
|
|
49
|
+
'seeking',
|
|
50
|
+
'stalled',
|
|
51
|
+
'suspend',
|
|
52
|
+
'timeupdate',
|
|
53
|
+
'volumechange',
|
|
54
|
+
'waiting',
|
|
55
|
+
'waitingforkey',
|
|
56
|
+
]);
|
|
27
57
|
|
|
28
58
|
/**
|
|
29
59
|
* Checks if an event should be delegated
|
|
30
60
|
* @param {string} event_name - The event name (e.g., 'click', 'focus')
|
|
31
61
|
* @returns {boolean}
|
|
32
62
|
*/
|
|
33
|
-
export function
|
|
34
|
-
|
|
63
|
+
export function is_non_delegated(event_name) {
|
|
64
|
+
return NON_DELEGATED_EVENTS.has(event_name);
|
|
35
65
|
}
|
|
36
66
|
|
|
37
67
|
/**
|
|
@@ -40,32 +70,79 @@ export function is_delegated(event_name) {
|
|
|
40
70
|
* @returns {boolean}
|
|
41
71
|
*/
|
|
42
72
|
export function is_event_attribute(attr) {
|
|
43
|
-
|
|
73
|
+
return attr.startsWith('on') && attr.length > 2 && attr[2] === attr[2].toUpperCase();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Checks if the event is a capture event.
|
|
78
|
+
* @param {string} event_name - The event name.
|
|
79
|
+
* @returns {boolean}
|
|
80
|
+
*/
|
|
81
|
+
export function is_capture_event(event_name) {
|
|
82
|
+
var lowered = event_name.toLowerCase();
|
|
83
|
+
return (
|
|
84
|
+
event_name.endsWith('Capture') &&
|
|
85
|
+
lowered !== 'gotpointercapture' &&
|
|
86
|
+
lowered !== 'lostpointercapture'
|
|
87
|
+
);
|
|
44
88
|
}
|
|
45
89
|
|
|
46
90
|
/**
|
|
91
|
+
* Retrieves the original event name from an event attribute.
|
|
47
92
|
* @param {string} name
|
|
93
|
+
* @returns {string}
|
|
48
94
|
*/
|
|
49
|
-
export function
|
|
50
|
-
|
|
51
|
-
name.endsWith('Capture') &&
|
|
52
|
-
name.toLowerCase() !== 'gotpointercapture' &&
|
|
53
|
-
name.toLowerCase() !== 'lostpointercapture'
|
|
54
|
-
);
|
|
95
|
+
export function get_original_event_name(name) {
|
|
96
|
+
return name.slice(2);
|
|
55
97
|
}
|
|
56
98
|
|
|
57
99
|
/**
|
|
100
|
+
* Normalizes the event name to lowercase.
|
|
101
|
+
* @param {string} name
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
export function normalize_event_name(name) {
|
|
105
|
+
return extract_event_name(name).toLowerCase();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extracts the base event name from an event attribute.
|
|
110
|
+
* @param {string} name
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
113
|
+
function extract_event_name(name) {
|
|
114
|
+
name = get_original_event_name(name);
|
|
115
|
+
|
|
116
|
+
if (is_capture_event(name)) {
|
|
117
|
+
return event_name_from_capture(name);
|
|
118
|
+
}
|
|
119
|
+
return name;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Converts a capture event name to its base event name.
|
|
58
124
|
* @param {string} event_name
|
|
125
|
+
* @returns {string}
|
|
59
126
|
*/
|
|
60
|
-
export function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
127
|
+
export function event_name_from_capture(event_name) {
|
|
128
|
+
return event_name.slice(0, -7); // strip "Capture"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Converts an event attribute name to the actual event name.
|
|
133
|
+
* @param {string} name
|
|
134
|
+
* @param {EventListener | AddEventObject} handler
|
|
135
|
+
* @returns {string}
|
|
136
|
+
*/
|
|
137
|
+
export function get_attribute_event_name(name, handler) {
|
|
138
|
+
name = extract_event_name(name);
|
|
139
|
+
|
|
140
|
+
return typeof handler === 'object' && handler.customName
|
|
141
|
+
? handler.customName
|
|
142
|
+
: name.toLowerCase();
|
|
66
143
|
}
|
|
67
144
|
|
|
68
|
-
const PASSIVE_EVENTS = ['touchstart', 'touchmove'];
|
|
145
|
+
const PASSIVE_EVENTS = ['touchstart', 'touchmove', 'wheel', 'mousewheel'];
|
|
69
146
|
|
|
70
147
|
/**
|
|
71
148
|
* Checks if an event is passive (e.g., 'touchstart', 'touchmove').
|
|
@@ -73,5 +150,5 @@ const PASSIVE_EVENTS = ['touchstart', 'touchmove'];
|
|
|
73
150
|
* @returns {boolean}
|
|
74
151
|
*/
|
|
75
152
|
export function is_passive_event(name) {
|
|
76
|
-
|
|
153
|
+
return PASSIVE_EVENTS.includes(name);
|
|
77
154
|
}
|
|
@@ -524,4 +524,446 @@ describe('basic client > attribute rendering', () => {
|
|
|
524
524
|
render(App);
|
|
525
525
|
expect(container).toMatchSnapshot();
|
|
526
526
|
});
|
|
527
|
+
|
|
528
|
+
it('handles reactive event handler changes', () => {
|
|
529
|
+
component Basic() {
|
|
530
|
+
let count = track(0);
|
|
531
|
+
let mode = track<'increment' | 'decrement'>('increment');
|
|
532
|
+
|
|
533
|
+
const incrementHandler = () => {
|
|
534
|
+
@count++;
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const decrementHandler = () => {
|
|
538
|
+
@count--;
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
<button
|
|
542
|
+
onClick={() => {
|
|
543
|
+
@mode = @mode === 'increment' ? 'decrement' : 'increment';
|
|
544
|
+
}}
|
|
545
|
+
class="toggle-mode"
|
|
546
|
+
>
|
|
547
|
+
{'Toggle Mode'}
|
|
548
|
+
</button>
|
|
549
|
+
<button onClick={@mode === 'increment' ? incrementHandler : decrementHandler} class="action">
|
|
550
|
+
{@mode === 'increment' ? '+' : '-'}
|
|
551
|
+
</button>
|
|
552
|
+
<div class="count">{@count}</div>
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
render(Basic);
|
|
556
|
+
|
|
557
|
+
const toggleBtn = container.querySelector('.toggle-mode');
|
|
558
|
+
const actionBtn = container.querySelector('.action');
|
|
559
|
+
const countDiv = container.querySelector('.count');
|
|
560
|
+
|
|
561
|
+
expect(actionBtn.textContent).toBe('+');
|
|
562
|
+
expect(countDiv.textContent).toBe('0');
|
|
563
|
+
|
|
564
|
+
actionBtn.click();
|
|
565
|
+
flushSync();
|
|
566
|
+
expect(countDiv.textContent).toBe('1');
|
|
567
|
+
|
|
568
|
+
actionBtn.click();
|
|
569
|
+
flushSync();
|
|
570
|
+
expect(countDiv.textContent).toBe('2');
|
|
571
|
+
|
|
572
|
+
toggleBtn.click();
|
|
573
|
+
flushSync();
|
|
574
|
+
expect(actionBtn.textContent).toBe('-');
|
|
575
|
+
|
|
576
|
+
actionBtn.click();
|
|
577
|
+
flushSync();
|
|
578
|
+
expect(countDiv.textContent).toBe('1');
|
|
579
|
+
|
|
580
|
+
actionBtn.click();
|
|
581
|
+
flushSync();
|
|
582
|
+
expect(countDiv.textContent).toBe('0');
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('handles events with capture option', () => {
|
|
586
|
+
component Basic() {
|
|
587
|
+
let captureOrder = #[];
|
|
588
|
+
|
|
589
|
+
const handleCaptureClick = {
|
|
590
|
+
handleEvent() {
|
|
591
|
+
captureOrder.push('capture');
|
|
592
|
+
},
|
|
593
|
+
capture: true,
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const handleBubbleClick = () => {
|
|
597
|
+
captureOrder.push('bubble');
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
<div onClick={handleCaptureClick} class="outer">
|
|
601
|
+
<button onClick={handleBubbleClick} class="inner">{'Click'}</button>
|
|
602
|
+
<div class="order">{captureOrder.join(' -> ')}</div>
|
|
603
|
+
</div>
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
render(Basic);
|
|
607
|
+
|
|
608
|
+
const button = container.querySelector('.inner');
|
|
609
|
+
const orderDiv = container.querySelector('.order');
|
|
610
|
+
|
|
611
|
+
expect(orderDiv.textContent).toBe('');
|
|
612
|
+
|
|
613
|
+
// Test that capture fires before bubble
|
|
614
|
+
button.click();
|
|
615
|
+
flushSync();
|
|
616
|
+
expect(orderDiv.textContent).toBe('capture -> bubble');
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('handles events with Capture suffix in the name', () => {
|
|
620
|
+
component Basic() {
|
|
621
|
+
let captureOrder = #[];
|
|
622
|
+
|
|
623
|
+
const handleCaptureClick = () => {
|
|
624
|
+
captureOrder.push('capture');
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const handleBubbleClick = () => {
|
|
628
|
+
captureOrder.push('bubble');
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
<div onClickCapture={handleCaptureClick} class="outer">
|
|
632
|
+
<button onClick={handleBubbleClick} class="inner">{'Click'}</button>
|
|
633
|
+
<div class="order">{captureOrder.join(' -> ')}</div>
|
|
634
|
+
</div>
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
render(Basic);
|
|
638
|
+
|
|
639
|
+
const button = container.querySelector('.inner');
|
|
640
|
+
const orderDiv = container.querySelector('.order');
|
|
641
|
+
|
|
642
|
+
expect(orderDiv.textContent).toBe('');
|
|
643
|
+
|
|
644
|
+
// Test that capture fires before bubble
|
|
645
|
+
button.click();
|
|
646
|
+
flushSync();
|
|
647
|
+
expect(orderDiv.textContent).toBe('capture -> bubble');
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('handles custom events with customName option', () => {
|
|
651
|
+
component Basic() {
|
|
652
|
+
let customEventCount = track(0);
|
|
653
|
+
|
|
654
|
+
const handleCustom = {
|
|
655
|
+
handleEvent(event: CustomEvent) {
|
|
656
|
+
@customEventCount += event.detail.value;
|
|
657
|
+
},
|
|
658
|
+
customName: 'MyCustomEvent',
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
<div>
|
|
662
|
+
<div onMyCustomEvent={handleCustom} class="custom-target">{'Custom'}</div>
|
|
663
|
+
<div class="custom-count">{@customEventCount}</div>
|
|
664
|
+
</div>
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
render(Basic);
|
|
668
|
+
|
|
669
|
+
const customTarget = container.querySelector('.custom-target');
|
|
670
|
+
const customCountDiv = container.querySelector('.custom-count');
|
|
671
|
+
|
|
672
|
+
expect(customCountDiv.textContent).toBe('0');
|
|
673
|
+
|
|
674
|
+
const customEvent = new CustomEvent('MyCustomEvent', { bubbles: true, detail: { value: 5 } });
|
|
675
|
+
customTarget.dispatchEvent(customEvent);
|
|
676
|
+
flushSync();
|
|
677
|
+
expect(customCountDiv.textContent).toBe('5');
|
|
678
|
+
|
|
679
|
+
const customEvent2 = new CustomEvent('MyCustomEvent', { bubbles: true, detail: { value: 3 } });
|
|
680
|
+
customTarget.dispatchEvent(customEvent2);
|
|
681
|
+
flushSync();
|
|
682
|
+
expect(customCountDiv.textContent).toBe('8');
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it('handles events with delegated: false option to bypass delegation', () => {
|
|
686
|
+
component Basic() {
|
|
687
|
+
let delegatedCount = track(0);
|
|
688
|
+
let nonDelegatedCount = track(0);
|
|
689
|
+
|
|
690
|
+
const delegatedHandler = () => {
|
|
691
|
+
@delegatedCount++;
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const nonDelegatedHandler = {
|
|
695
|
+
handleEvent() {
|
|
696
|
+
@nonDelegatedCount++;
|
|
697
|
+
},
|
|
698
|
+
delegated: false,
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
<div>
|
|
702
|
+
<button onClick={delegatedHandler} class="delegated-btn">{'Delegated'}</button>
|
|
703
|
+
<button onClick={nonDelegatedHandler} class="non-delegated-btn">{'Non-Delegated'}</button>
|
|
704
|
+
<div class="delegated-count">{@delegatedCount}</div>
|
|
705
|
+
<div class="non-delegated-count">{@nonDelegatedCount}</div>
|
|
706
|
+
</div>
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
render(Basic);
|
|
710
|
+
|
|
711
|
+
const delegatedBtn = container.querySelector('.delegated-btn');
|
|
712
|
+
const nonDelegatedBtn = container.querySelector('.non-delegated-btn');
|
|
713
|
+
|
|
714
|
+
// Check that delegated event has __click property set on the element
|
|
715
|
+
expect(delegatedBtn.__click).toBeDefined();
|
|
716
|
+
expect(typeof delegatedBtn.__click).toBe('function');
|
|
717
|
+
|
|
718
|
+
// Check that non-delegated event does NOT have __click property (it's attached directly)
|
|
719
|
+
expect(nonDelegatedBtn.__click).toBeUndefined();
|
|
720
|
+
|
|
721
|
+
// Verify both handlers work correctly
|
|
722
|
+
delegatedBtn.click();
|
|
723
|
+
flushSync();
|
|
724
|
+
expect(container.querySelector('.delegated-count').textContent).toBe('1');
|
|
725
|
+
|
|
726
|
+
nonDelegatedBtn.click();
|
|
727
|
+
flushSync();
|
|
728
|
+
expect(container.querySelector('.non-delegated-count').textContent).toBe('1');
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it('measures effect of delegated vs non-delegated events', () => {
|
|
732
|
+
const delegatedClicks: number[] = [];
|
|
733
|
+
const nonDelegatedClicks: number[] = [];
|
|
734
|
+
|
|
735
|
+
component Basic() {
|
|
736
|
+
const makeDelegatedHandler = (id: number) => () => {
|
|
737
|
+
delegatedClicks.push(id);
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
const makeNonDelegatedHandler = (id: number) => ({
|
|
741
|
+
handleEvent() {
|
|
742
|
+
nonDelegatedClicks.push(id);
|
|
743
|
+
},
|
|
744
|
+
delegated: false,
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
const buttonIds = [0, 1, 2, 3, 4];
|
|
748
|
+
|
|
749
|
+
<div>
|
|
750
|
+
<div class="delegated-buttons">
|
|
751
|
+
for (const i of buttonIds) {
|
|
752
|
+
<button onClick={makeDelegatedHandler(i)} class={`delegated-${i}`}>{`D${i}`}</button>
|
|
753
|
+
}
|
|
754
|
+
</div>
|
|
755
|
+
<div class="non-delegated-buttons">
|
|
756
|
+
for (const i of buttonIds) {
|
|
757
|
+
<button onClick={makeNonDelegatedHandler(i)} class={`non-delegated-${i}`}>
|
|
758
|
+
{`ND${i}`}
|
|
759
|
+
</button>
|
|
760
|
+
}
|
|
761
|
+
</div>
|
|
762
|
+
</div>
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
render(Basic);
|
|
766
|
+
|
|
767
|
+
// Test delegated buttons
|
|
768
|
+
for (let i = 0; i < 5; i++) {
|
|
769
|
+
const btn = container.querySelector(`.delegated-${i}`);
|
|
770
|
+
btn.click();
|
|
771
|
+
flushSync();
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
expect(delegatedClicks).toEqual([0, 1, 2, 3, 4]);
|
|
775
|
+
|
|
776
|
+
// Test non-delegated buttons
|
|
777
|
+
for (let i = 0; i < 5; i++) {
|
|
778
|
+
const btn = container.querySelector(`.non-delegated-${i}`);
|
|
779
|
+
btn.click();
|
|
780
|
+
flushSync();
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
expect(nonDelegatedClicks).toEqual([0, 1, 2, 3, 4]);
|
|
784
|
+
|
|
785
|
+
// Verify both can be called in mixed order
|
|
786
|
+
delegatedClicks.length = 0;
|
|
787
|
+
nonDelegatedClicks.length = 0;
|
|
788
|
+
|
|
789
|
+
container.querySelector('.delegated-2').click();
|
|
790
|
+
container.querySelector('.non-delegated-1').click();
|
|
791
|
+
container.querySelector('.delegated-0').click();
|
|
792
|
+
container.querySelector('.non-delegated-3').click();
|
|
793
|
+
flushSync();
|
|
794
|
+
|
|
795
|
+
expect(delegatedClicks).toEqual([2, 0]);
|
|
796
|
+
expect(nonDelegatedClicks).toEqual([1, 3]);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it('handles events defined as function directly vs as object', () => {
|
|
800
|
+
component Basic() {
|
|
801
|
+
let functionCount = track(0);
|
|
802
|
+
let objectCount = track(0);
|
|
803
|
+
|
|
804
|
+
const functionHandler = () => {
|
|
805
|
+
@functionCount++;
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
const objectHandler = {
|
|
809
|
+
handleEvent() {
|
|
810
|
+
@objectCount++;
|
|
811
|
+
},
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
<div>
|
|
815
|
+
<button onClick={functionHandler} class="function-btn">{'Function'}</button>
|
|
816
|
+
<button onClick={objectHandler} class="object-btn">{'Object'}</button>
|
|
817
|
+
<div class="function-count">{@functionCount}</div>
|
|
818
|
+
<div class="object-count">{@objectCount}</div>
|
|
819
|
+
</div>
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
render(Basic);
|
|
823
|
+
|
|
824
|
+
const functionBtn = container.querySelector('.function-btn');
|
|
825
|
+
const objectBtn = container.querySelector('.object-btn');
|
|
826
|
+
const functionCountDiv = container.querySelector('.function-count');
|
|
827
|
+
const objectCountDiv = container.querySelector('.object-count');
|
|
828
|
+
|
|
829
|
+
expect(functionCountDiv.textContent).toBe('0');
|
|
830
|
+
expect(objectCountDiv.textContent).toBe('0');
|
|
831
|
+
|
|
832
|
+
functionBtn.click();
|
|
833
|
+
flushSync();
|
|
834
|
+
expect(functionCountDiv.textContent).toBe('1');
|
|
835
|
+
|
|
836
|
+
objectBtn.click();
|
|
837
|
+
flushSync();
|
|
838
|
+
expect(objectCountDiv.textContent).toBe('1');
|
|
839
|
+
|
|
840
|
+
// Test multiple clicks
|
|
841
|
+
functionBtn.click();
|
|
842
|
+
functionBtn.click();
|
|
843
|
+
flushSync();
|
|
844
|
+
expect(functionCountDiv.textContent).toBe('3');
|
|
845
|
+
|
|
846
|
+
objectBtn.click();
|
|
847
|
+
objectBtn.click();
|
|
848
|
+
flushSync();
|
|
849
|
+
expect(objectCountDiv.textContent).toBe('3');
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it('handles passive event option', () => {
|
|
853
|
+
component Basic() {
|
|
854
|
+
let passiveDefaultPrevented = track<boolean | null>(null);
|
|
855
|
+
let nonPassiveDefaultPrevented = track<boolean | null>(null);
|
|
856
|
+
|
|
857
|
+
const passiveHandler = {
|
|
858
|
+
handleEvent(event: Event) {
|
|
859
|
+
event.preventDefault();
|
|
860
|
+
// In passive listeners, preventDefault() is ignored
|
|
861
|
+
@passiveDefaultPrevented = event.defaultPrevented;
|
|
862
|
+
},
|
|
863
|
+
passive: true,
|
|
864
|
+
delegated: false, // Need to ensure it's not delegated to test passive properly
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
const nonPassiveHandler = {
|
|
868
|
+
handleEvent(event: Event) {
|
|
869
|
+
event.preventDefault();
|
|
870
|
+
// In non-passive listeners, preventDefault() works
|
|
871
|
+
@nonPassiveDefaultPrevented = event.defaultPrevented;
|
|
872
|
+
},
|
|
873
|
+
delegated: false,
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
<div>
|
|
877
|
+
<div onWheel={passiveHandler} class="passive-target">{'Passive'}</div>
|
|
878
|
+
<div onWheel={nonPassiveHandler} class="non-passive-target">{'Non-Passive'}</div>
|
|
879
|
+
<div class="passive-result">
|
|
880
|
+
{@passiveDefaultPrevented === null
|
|
881
|
+
? 'not-tested'
|
|
882
|
+
: @passiveDefaultPrevented
|
|
883
|
+
? 'prevented'
|
|
884
|
+
: 'not-prevented'}
|
|
885
|
+
</div>
|
|
886
|
+
<div class="non-passive-result">
|
|
887
|
+
{@nonPassiveDefaultPrevented === null
|
|
888
|
+
? 'not-tested'
|
|
889
|
+
: @nonPassiveDefaultPrevented
|
|
890
|
+
? 'prevented'
|
|
891
|
+
: 'not-prevented'}
|
|
892
|
+
</div>
|
|
893
|
+
</div>
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
render(Basic);
|
|
897
|
+
|
|
898
|
+
const passiveTarget = container.querySelector('.passive-target');
|
|
899
|
+
const nonPassiveTarget = container.querySelector('.non-passive-target');
|
|
900
|
+
const passiveResultDiv = container.querySelector('.passive-result');
|
|
901
|
+
const nonPassiveResultDiv = container.querySelector('.non-passive-result');
|
|
902
|
+
|
|
903
|
+
expect(passiveResultDiv.textContent).toBe('not-tested');
|
|
904
|
+
expect(nonPassiveResultDiv.textContent).toBe('not-tested');
|
|
905
|
+
|
|
906
|
+
// Test passive event - preventDefault should be ignored, defaultPrevented stays false
|
|
907
|
+
passiveTarget.dispatchEvent(new WheelEvent('wheel', { bubbles: true, cancelable: true }));
|
|
908
|
+
flushSync();
|
|
909
|
+
expect(passiveResultDiv.textContent).toBe('not-prevented');
|
|
910
|
+
|
|
911
|
+
// Test non-passive event - preventDefault should work, defaultPrevented becomes true
|
|
912
|
+
nonPassiveTarget.dispatchEvent(new WheelEvent('wheel', { bubbles: true, cancelable: true }));
|
|
913
|
+
flushSync();
|
|
914
|
+
expect(nonPassiveResultDiv.textContent).toBe('prevented');
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it('handles once option to fire event only once', () => {
|
|
918
|
+
component Basic() {
|
|
919
|
+
let onceCount = track(0);
|
|
920
|
+
let regularCount = track(0);
|
|
921
|
+
|
|
922
|
+
const onceHandler = {
|
|
923
|
+
handleEvent() {
|
|
924
|
+
@onceCount++;
|
|
925
|
+
},
|
|
926
|
+
once: true,
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
const regularHandler = () => {
|
|
930
|
+
@regularCount++;
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
<div>
|
|
934
|
+
<button onClick={onceHandler} class="once-btn">{'Once'}</button>
|
|
935
|
+
<button onClick={regularHandler} class="regular-btn">{'Regular'}</button>
|
|
936
|
+
<div class="once-count">{@onceCount}</div>
|
|
937
|
+
<div class="regular-count">{@regularCount}</div>
|
|
938
|
+
</div>
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
render(Basic);
|
|
942
|
+
|
|
943
|
+
const onceBtn = container.querySelector('.once-btn');
|
|
944
|
+
const regularBtn = container.querySelector('.regular-btn');
|
|
945
|
+
const onceCountDiv = container.querySelector('.once-count');
|
|
946
|
+
const regularCountDiv = container.querySelector('.regular-count');
|
|
947
|
+
|
|
948
|
+
expect(onceCountDiv.textContent).toBe('0');
|
|
949
|
+
expect(regularCountDiv.textContent).toBe('0');
|
|
950
|
+
|
|
951
|
+
onceBtn.click();
|
|
952
|
+
flushSync();
|
|
953
|
+
expect(onceCountDiv.textContent).toBe('1');
|
|
954
|
+
|
|
955
|
+
// Second click should not increment because of once: true
|
|
956
|
+
onceBtn.click();
|
|
957
|
+
flushSync();
|
|
958
|
+
expect(onceCountDiv.textContent).toBe('1');
|
|
959
|
+
|
|
960
|
+
// Regular handler should work multiple times
|
|
961
|
+
regularBtn.click();
|
|
962
|
+
flushSync();
|
|
963
|
+
expect(regularCountDiv.textContent).toBe('1');
|
|
964
|
+
|
|
965
|
+
regularBtn.click();
|
|
966
|
+
flushSync();
|
|
967
|
+
expect(regularCountDiv.textContent).toBe('2');
|
|
968
|
+
});
|
|
527
969
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Tracked } from 'ripple';
|
|
1
2
|
import { track, trackSplit, flushSync, effect } from 'ripple';
|
|
2
3
|
|
|
3
4
|
describe('composite > reactivity', () => {
|
|
@@ -277,7 +278,7 @@ describe('composite > reactivity', () => {
|
|
|
277
278
|
const more = #{
|
|
278
279
|
double: track(() => props.@count * 2),
|
|
279
280
|
another: track(0),
|
|
280
|
-
|
|
281
|
+
extra: 100,
|
|
281
282
|
};
|
|
282
283
|
|
|
283
284
|
effect(() => {
|
|
@@ -290,7 +291,7 @@ describe('composite > reactivity', () => {
|
|
|
290
291
|
});
|
|
291
292
|
|
|
292
293
|
<div>
|
|
293
|
-
<Counter {...props} double={more.double} another={more.another}
|
|
294
|
+
<Counter {...props} double={more.double} another={more.another} extra={more.extra} />
|
|
294
295
|
</div>
|
|
295
296
|
}
|
|
296
297
|
|
|
@@ -307,7 +308,7 @@ describe('composite > reactivity', () => {
|
|
|
307
308
|
|
|
308
309
|
expect(div.getAttribute('double')).toBe('0');
|
|
309
310
|
expect(div.getAttribute('another')).toBe('0');
|
|
310
|
-
expect(div.getAttribute('
|
|
311
|
+
expect(div.getAttribute('extra')).toBe('100');
|
|
311
312
|
expect(div.textContent).toBe('Counter: 0 Double: 0');
|
|
312
313
|
|
|
313
314
|
buttonIncrement.click();
|
|
@@ -315,7 +316,7 @@ describe('composite > reactivity', () => {
|
|
|
315
316
|
|
|
316
317
|
expect(div.getAttribute('double')).toBe('2');
|
|
317
318
|
expect(div.hasAttribute('another')).toBe(false);
|
|
318
|
-
expect(div.getAttribute('
|
|
319
|
+
expect(div.getAttribute('extra')).toBe('100');
|
|
319
320
|
expect(div.textContent).toBe('Counter: 1 Double: 2');
|
|
320
321
|
|
|
321
322
|
buttonIncrement.click();
|
|
@@ -323,7 +324,7 @@ describe('composite > reactivity', () => {
|
|
|
323
324
|
|
|
324
325
|
expect(div.getAttribute('double')).toBe('4');
|
|
325
326
|
expect(div.getAttribute('another')).toBe('0');
|
|
326
|
-
expect(div.getAttribute('
|
|
327
|
+
expect(div.getAttribute('extra')).toBe('100');
|
|
327
328
|
expect(div.textContent).toBe('Counter: 2 Double: 4');
|
|
328
329
|
});
|
|
329
330
|
});
|