ripple 0.2.91 → 0.2.93
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 +1 -1
- package/src/compiler/index.js +17 -10
- package/src/compiler/phases/1-parse/index.js +55 -7
- package/src/compiler/phases/3-transform/client/index.js +82 -55
- package/src/compiler/phases/3-transform/segments.js +422 -224
- package/src/compiler/scope.js +478 -404
- package/src/compiler/types/index.d.ts +299 -3
- package/src/compiler/utils.js +173 -30
- package/src/runtime/index-client.js +1 -0
- package/src/runtime/internal/client/html.js +18 -8
- package/src/runtime/internal/client/index.js +1 -0
- package/src/runtime/internal/client/portal.js +55 -32
- package/src/runtime/internal/client/render.js +31 -1
- package/src/runtime/internal/client/runtime.js +53 -22
- package/src/utils/normalize_css_property_name.js +23 -0
- package/tests/client/basic.test.ripple +207 -1
- package/tests/client/compiler.test.ripple +95 -1
- package/tests/client/html.test.ripple +29 -1
- package/tests/client/portal.test.ripple +167 -0
- package/types/index.d.ts +4 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mount, Portal, track, flushSync } from 'ripple';
|
|
3
|
+
|
|
4
|
+
describe('Portal', () => {
|
|
5
|
+
let container;
|
|
6
|
+
|
|
7
|
+
function render(component) {
|
|
8
|
+
mount(component, {
|
|
9
|
+
target: container,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
container = document.createElement('div');
|
|
15
|
+
document.body.appendChild(container);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
// Remove container
|
|
20
|
+
document.body.removeChild(container);
|
|
21
|
+
|
|
22
|
+
// Clean up any leftover portal content from document.body
|
|
23
|
+
const portals = document.body.querySelectorAll('.test-portal');
|
|
24
|
+
portals.forEach(el => el.remove());
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('renders portal content to target element', () => {
|
|
28
|
+
const target = document.createElement('div');
|
|
29
|
+
document.body.appendChild(target);
|
|
30
|
+
|
|
31
|
+
component TestPortal() {
|
|
32
|
+
<Portal target={target}><div class='test-portal'>{'Portal works!'}</div></Portal>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
render(TestPortal);
|
|
36
|
+
|
|
37
|
+
// Portal content should be in the target, not in container
|
|
38
|
+
expect(container.querySelector('.test-portal')).toBeNull();
|
|
39
|
+
expect(target.querySelector('.test-portal')).toBeTruthy();
|
|
40
|
+
expect(target.querySelector('.test-portal').textContent).toBe('Portal works!');
|
|
41
|
+
|
|
42
|
+
document.body.removeChild(target);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('renders portal content to document.body', () => {
|
|
46
|
+
component TestPortal() {
|
|
47
|
+
<Portal target={document.body}><div class='test-portal'>{'In document.body!'}</div></Portal>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
render(TestPortal);
|
|
51
|
+
|
|
52
|
+
// Should not be in container
|
|
53
|
+
expect(container.querySelector('.test-portal')).toBeNull();
|
|
54
|
+
|
|
55
|
+
// Should be in document.body
|
|
56
|
+
expect(document.body.querySelector('.test-portal')).toBeTruthy();
|
|
57
|
+
expect(document.body.querySelector('.test-portal').textContent).toBe('In document.body!');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('cleans up portal content when destroyed via conditional rendering', () => {
|
|
61
|
+
component TestPortal() {
|
|
62
|
+
let open = track(true);
|
|
63
|
+
|
|
64
|
+
if (@open) {
|
|
65
|
+
<Portal target={document.body}><div class='test-portal'>{'Conditional content'}</div></Portal>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
<button onClick={() => @open = false}>{'Close'}</button>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
render(TestPortal);
|
|
72
|
+
|
|
73
|
+
// Initially portal content should be present
|
|
74
|
+
expect(document.body.querySelector('.test-portal')).toBeTruthy();
|
|
75
|
+
|
|
76
|
+
// Click close button to destroy portal
|
|
77
|
+
container.querySelector('button').click();
|
|
78
|
+
flushSync();
|
|
79
|
+
|
|
80
|
+
// Portal content should be cleaned up
|
|
81
|
+
expect(document.body.querySelector('.test-portal')).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('opens and closes portal via conditional rendering', () => {
|
|
85
|
+
component TestPortal() {
|
|
86
|
+
let open = track(false);
|
|
87
|
+
|
|
88
|
+
if (@open) {
|
|
89
|
+
<Portal target={document.body}><div class='test-portal'>
|
|
90
|
+
{'Content'}
|
|
91
|
+
<button onClick={() => @open = false}>{'Close'}</button>
|
|
92
|
+
</div></Portal>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!@open) {
|
|
96
|
+
<button onClick={() => @open = true}>{'Open'}</button>
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
render(TestPortal);
|
|
101
|
+
|
|
102
|
+
// Open the portal
|
|
103
|
+
container.querySelector('button').click();
|
|
104
|
+
flushSync();
|
|
105
|
+
expect(document.body.querySelector('.test-portal')).toBeTruthy();
|
|
106
|
+
|
|
107
|
+
// Close the portal - this should work without errors
|
|
108
|
+
expect(() => {
|
|
109
|
+
document.body.querySelector('button').click();
|
|
110
|
+
flushSync();
|
|
111
|
+
}).not.toThrow();
|
|
112
|
+
|
|
113
|
+
// Portal content should be cleaned up
|
|
114
|
+
expect(document.body.querySelector('.test-portal')).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('handles multiple portals simultaneously', () => {
|
|
118
|
+
const target1 = document.createElement('div');
|
|
119
|
+
const target2 = document.createElement('div');
|
|
120
|
+
target1.id = 'multi-target1';
|
|
121
|
+
target2.id = 'multi-target2';
|
|
122
|
+
document.body.appendChild(target1);
|
|
123
|
+
document.body.appendChild(target2);
|
|
124
|
+
|
|
125
|
+
component TestMultiPortal() {
|
|
126
|
+
<Portal target={target1}><div class='test-portal'>{'Portal 1 content'}</div></Portal>
|
|
127
|
+
|
|
128
|
+
<Portal target={target2}><div class='test-portal'>{'Portal 2 content'}</div></Portal>
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
render(TestMultiPortal);
|
|
132
|
+
|
|
133
|
+
// Both portals should render in their respective targets
|
|
134
|
+
expect(target1.querySelector('.test-portal')).toBeTruthy();
|
|
135
|
+
expect(target1.querySelector('.test-portal').textContent).toBe('Portal 1 content');
|
|
136
|
+
|
|
137
|
+
expect(target2.querySelector('.test-portal')).toBeTruthy();
|
|
138
|
+
expect(target2.querySelector('.test-portal').textContent).toBe('Portal 2 content');
|
|
139
|
+
|
|
140
|
+
document.body.removeChild(target1);
|
|
141
|
+
document.body.removeChild(target2);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('handles portal with reactive content', () => {
|
|
145
|
+
component TestReactivePortal() {
|
|
146
|
+
let count = track(0);
|
|
147
|
+
|
|
148
|
+
<Portal target={document.body}><div class='test-portal'>
|
|
149
|
+
{'Count: '}
|
|
150
|
+
{String(@count)}
|
|
151
|
+
<button onClick={() => @count++}>{'Increment'}</button>
|
|
152
|
+
</div></Portal>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
render(TestReactivePortal);
|
|
156
|
+
|
|
157
|
+
const portalElement = document.body.querySelector('.test-portal');
|
|
158
|
+
expect(portalElement).toBeTruthy();
|
|
159
|
+
expect(portalElement.textContent).toContain('Count: 0');
|
|
160
|
+
|
|
161
|
+
// Click increment button
|
|
162
|
+
portalElement.querySelector('button').click();
|
|
163
|
+
flushSync();
|
|
164
|
+
|
|
165
|
+
expect(portalElement.textContent).toContain('Count: 1');
|
|
166
|
+
});
|
|
167
|
+
});
|
package/types/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export declare function mount(
|
|
|
5
5
|
options: { target: HTMLElement; props?: Record<string, any> },
|
|
6
6
|
): () => void;
|
|
7
7
|
|
|
8
|
+
export declare function tick(): Promise<void>;
|
|
9
|
+
|
|
8
10
|
export declare function untrack<T>(fn: () => T): T;
|
|
9
11
|
|
|
10
12
|
export declare function flushSync<T>(fn: () => T): T;
|
|
@@ -166,3 +168,5 @@ export class SvelteDate extends Date {
|
|
|
166
168
|
constructor(...params: any[]);
|
|
167
169
|
#private;
|
|
168
170
|
}
|
|
171
|
+
|
|
172
|
+
export function Portal<V = HTMLElement>({ target, children: Component }: { target: V }): void;
|