Skip to content

Commit 20ee0e9

Browse files
authored
Add viewport modifier to resize plugin (#4910)
* Add viewport modifier to resize plugin Assisted-By: devx/01a80b4a-afc0-4fff-ab03-cfaf238bf0d9 * Ensure viewport fallback initializes independently Assisted-By: devx/01a80b4a-afc0-4fff-ab03-cfaf238bf0d9 * Register viewport cleanup before evaluation Assisted-By: devx/01a80b4a-afc0-4fff-ab03-cfaf238bf0d9
1 parent c7fa1bf commit 20ee0e9

3 files changed

Lines changed: 131 additions & 0 deletions

File tree

‎packages/docs/src/en/plugins/resize.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,19 @@ For example, here's a simple example of using `x-resize` to display the width an
7878
<a name="modifiers"></a>
7979
## Modifiers
8080

81+
<a name="viewport"></a>
82+
### .viewport
83+
84+
The visual viewport is the portion of the page currently visible on screen. On mobile browsers, it can change independently of the document when browser controls or the on-screen keyboard appear. Use the `.viewport` modifier to react to these changes using `window.visualViewport`:
85+
86+
```alpine
87+
<div x-resize.viewport="width = $width; height = $height">
88+
...
89+
</div>
90+
```
91+
92+
`$width` and `$height` contain the visual viewport's dimensions. In browsers where `window.visualViewport` is unavailable, Alpine falls back to observing the document.
93+
8194
<a name="document"></a>
8295
### .document
8396

‎packages/resize/src/index.js‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ export default function (Alpine) {
66
evaluator(() => {}, { scope: { '$width': width, '$height': height }})
77
}
88

9+
if (modifiers.includes('viewport')) {
10+
onViewportResize(evaluate, cleanup)
11+
12+
return
13+
}
14+
915
let off = modifiers.includes('document')
1016
? onDocumentResize(evaluate)
1117
: onElResize(el, evaluate)
@@ -26,6 +32,22 @@ function onElResize(el, callback) {
2632
return () => observer.disconnect()
2733
}
2834

35+
function onViewportResize(callback, cleanup) {
36+
let viewport = window.visualViewport
37+
38+
if (! viewport) {
39+
cleanup(onElResize(document.documentElement, callback))
40+
41+
return
42+
}
43+
44+
let evaluate = () => callback(viewport.width, viewport.height)
45+
46+
viewport.addEventListener('resize', evaluate)
47+
cleanup(() => viewport.removeEventListener('resize', evaluate))
48+
evaluate()
49+
}
50+
2951
let documentResizeObserver
3052
let documentResizeObserverCallbacks = new Set
3153

‎tests/cypress/integration/plugins/resize.spec.js‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,99 @@ test('can react to the resizing of the document',
4646
get('h2').should(haveText('750'))
4747
},
4848
)
49+
50+
test('can react to the resizing of the visual viewport',
51+
[html`
52+
<div x-data="{ width: 0, height: 0 }">
53+
<h1 x-text="width"></h1>
54+
<h2 x-text="height"></h2>
55+
56+
<div x-resize.viewport="width = $width; height = $height"></div>
57+
58+
<button @click="window.testViewport.width = 400; window.testViewport.height = 600; window.testViewport.dispatchEvent(new Event('resize'))">resize</button>
59+
</div>
60+
`, `
61+
window.testViewport = Object.assign(new EventTarget(), { width: 320, height: 480 })
62+
Object.defineProperty(window, 'visualViewport', { configurable: true, value: window.testViewport })
63+
`],
64+
({ get }) => {
65+
get('h1').should(haveText('320'))
66+
get('h2').should(haveText('480'))
67+
68+
get('button').click()
69+
70+
get('h1').should(haveText('400'))
71+
get('h2').should(haveText('600'))
72+
},
73+
)
74+
75+
test('removes the visual viewport listener when the directive is cleaned up',
76+
[html`
77+
<div x-data="{ shown: true, count: 0 }">
78+
<h1 x-text="count"></h1>
79+
80+
<template x-if="shown">
81+
<div x-resize.viewport="count++"></div>
82+
</template>
83+
84+
<button id="remove" @click="shown = false">remove</button>
85+
<button id="resize" @click="window.testViewport.dispatchEvent(new Event('resize'))">resize</button>
86+
</div>
87+
`, `
88+
window.testViewport = Object.assign(new EventTarget(), { width: 320, height: 480 })
89+
Object.defineProperty(window, 'visualViewport', { configurable: true, value: window.testViewport })
90+
`],
91+
({ get }) => {
92+
get('h1').should(haveText('1'))
93+
94+
get('button#remove').click()
95+
get('[x-resize\\.viewport]').should('not.exist')
96+
get('button#resize').click()
97+
98+
get('h1').should(haveText('1'))
99+
},
100+
)
101+
102+
test('removes the visual viewport listener during its initial evaluation',
103+
[html`
104+
<div x-data="{ count: 0 }">
105+
<h1 x-text="count"></h1>
106+
107+
<div x-resize.viewport="count++; window.Alpine.destroyTree($el)"></div>
108+
109+
<button @click="window.testViewport.dispatchEvent(new Event('resize'))">resize</button>
110+
</div>
111+
`, `
112+
window.testViewport = Object.assign(new EventTarget(), { width: 320, height: 480 })
113+
Object.defineProperty(window, 'visualViewport', { configurable: true, value: window.testViewport })
114+
`],
115+
({ get }) => {
116+
get('h1').should(haveText('1'))
117+
118+
get('button').click()
119+
120+
get('h1').should(haveText('1'))
121+
},
122+
)
123+
124+
test('falls back to observing the document when visualViewport is unavailable',
125+
[html`
126+
<div x-data="{ width: 0, height: 0 }">
127+
<h1 x-text="width"></h1>
128+
<h2 x-text="height"></h2>
129+
130+
<div x-resize.viewport="width = $width; height = $height"></div>
131+
</div>
132+
`, `
133+
Object.defineProperty(window, 'visualViewport', { configurable: true, value: null })
134+
`],
135+
({ get }) => {
136+
get('h1').should(notHaveText('0'))
137+
get('h2').should(notHaveText('0'))
138+
139+
cy.viewport(550, 750)
140+
141+
get('h1').should(haveText('550'))
142+
get('h2').should(haveText('750'))
143+
},
144+
)

0 commit comments

Comments
 (0)