@saasmakers/ui 2.0.78 → 2.0.80
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.
|
@@ -18,6 +18,8 @@ const visible = defineModel<boolean>({ default: false })
|
|
|
18
18
|
const contentRef = ref<HTMLElement>()
|
|
19
19
|
const panelRef = ref<HTMLElement>()
|
|
20
20
|
const { release: onAfterLeave } = useDialog(visible, panelRef)
|
|
21
|
+
const { isKeyboardOpen, keyboardInset } = useKeyboardInset()
|
|
22
|
+
const skipLeaveSlide = ref(false)
|
|
21
23
|
|
|
22
24
|
const drag = reactive({
|
|
23
25
|
closing: false,
|
|
@@ -32,6 +34,30 @@ const closeThreshold = computed(() => {
|
|
|
32
34
|
return (panelRef.value?.offsetHeight ?? 320) * closeThresholdRatio
|
|
33
35
|
})
|
|
34
36
|
|
|
37
|
+
// Use `bottom` (not transform) so keyboard lift does not fight drag-to-dismiss.
|
|
38
|
+
const panelStyle = computed(() => {
|
|
39
|
+
return {
|
|
40
|
+
bottom: `${keyboardInset.value}px`,
|
|
41
|
+
maxHeight: isKeyboardOpen.value
|
|
42
|
+
? `calc(100dvh - ${keyboardInset.value}px - env(safe-area-inset-top))`
|
|
43
|
+
: undefined,
|
|
44
|
+
transform: drag.closing ? 'translateY(100%)' : drag.offset > 0 ? `translateY(${drag.offset}px)` : undefined,
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
function dismissKeyboard() {
|
|
49
|
+
if (!import.meta.client) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const activeElement = document.activeElement
|
|
54
|
+
|
|
55
|
+
// Blur instead of Keyboard.hide() so focus is cleared too (incl. the warmup input).
|
|
56
|
+
if (activeElement instanceof HTMLElement && activeElement !== document.body) {
|
|
57
|
+
activeElement.blur()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
35
61
|
function onBackdropClick() {
|
|
36
62
|
if (props.closeOnBackdrop) {
|
|
37
63
|
onClose()
|
|
@@ -72,6 +98,12 @@ function onHeaderPointerDown(event: PointerEvent) {
|
|
|
72
98
|
handle.setPointerCapture(event.pointerId)
|
|
73
99
|
}
|
|
74
100
|
|
|
101
|
+
function onLeaveComplete() {
|
|
102
|
+
skipLeaveSlide.value = false
|
|
103
|
+
|
|
104
|
+
onAfterLeave()
|
|
105
|
+
}
|
|
106
|
+
|
|
75
107
|
function onPanelTransitionEnd(event: TransitionEvent) {
|
|
76
108
|
if (event.propertyName !== 'transform' || !drag.closing) {
|
|
77
109
|
return
|
|
@@ -156,6 +188,13 @@ function onPointerUp(event: PointerEvent) {
|
|
|
156
188
|
handle.releasePointerCapture(event.pointerId)
|
|
157
189
|
|
|
158
190
|
if (drag.offset >= closeThreshold.value) {
|
|
191
|
+
if (isKeyboardOpen.value) {
|
|
192
|
+
onClose()
|
|
193
|
+
resetDragState()
|
|
194
|
+
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
|
|
159
198
|
drag.closing = true
|
|
160
199
|
|
|
161
200
|
return
|
|
@@ -180,6 +219,10 @@ function resetPendingDrag() {
|
|
|
180
219
|
|
|
181
220
|
watch(visible, (isVisible) => {
|
|
182
221
|
if (!isVisible) {
|
|
222
|
+
// iOS hides the keyboard before a leave slide would finish.
|
|
223
|
+
skipLeaveSlide.value = isKeyboardOpen.value
|
|
224
|
+
|
|
225
|
+
dismissKeyboard()
|
|
183
226
|
resetDragState()
|
|
184
227
|
}
|
|
185
228
|
})
|
|
@@ -197,28 +240,28 @@ watch(visible, (isVisible) => {
|
|
|
197
240
|
<ClientOnly>
|
|
198
241
|
<Teleport to="body">
|
|
199
242
|
<Transition
|
|
243
|
+
:css="!skipLeaveSlide"
|
|
200
244
|
enter-active-class="transition-transform duration-300 ease motion-reduce:transition-none motion-reduce:duration-0"
|
|
201
245
|
enter-from-class="translate-y-full motion-reduce:translate-y-0"
|
|
202
246
|
enter-to-class="translate-y-0"
|
|
203
247
|
leave-active-class="transition-transform duration-300 ease motion-reduce:transition-none motion-reduce:duration-0"
|
|
204
248
|
leave-from-class="translate-y-0"
|
|
205
249
|
leave-to-class="translate-y-full motion-reduce:translate-y-0"
|
|
206
|
-
@after-leave="
|
|
250
|
+
@after-leave="onLeaveComplete"
|
|
207
251
|
>
|
|
208
252
|
<div
|
|
209
253
|
v-if="visible"
|
|
210
254
|
ref="panelRef"
|
|
211
255
|
:aria-label="props['aria-label']"
|
|
212
256
|
aria-modal="true"
|
|
213
|
-
class="layout-bottom-sheet fixed bottom-0 left-0 right-0 z-[60] mx-auto max-h-[85dvh] max-w-screen-md flex flex-col rounded-t-2xl bg-white text-gray-900 app-text shadow-lg outline-none
|
|
257
|
+
class="layout-bottom-sheet fixed bottom-0 left-0 right-0 z-[60] mx-auto max-h-[85dvh] max-w-screen-md flex flex-col rounded-t-2xl bg-white text-gray-900 app-text shadow-lg outline-none dark:bg-gray-900 dark:text-gray-100"
|
|
214
258
|
:class="{
|
|
259
|
+
'safe-bottom': !isKeyboardOpen,
|
|
215
260
|
'transition-none motion-reduce:transition-none': drag.isDragging,
|
|
216
|
-
'transition-transform duration-
|
|
261
|
+
'transition-[bottom,max-height,transform] duration-250 ease-out motion-reduce:transition-none motion-reduce:duration-0': !drag.isDragging,
|
|
217
262
|
}"
|
|
263
|
+
:style="panelStyle"
|
|
218
264
|
role="dialog"
|
|
219
|
-
:style="{
|
|
220
|
-
transform: drag.closing ? 'translateY(100%)' : drag.offset > 0 ? `translateY(${drag.offset}px)` : undefined,
|
|
221
|
-
}"
|
|
222
265
|
tabindex="-1"
|
|
223
266
|
@transitionend="onPanelTransitionEnd"
|
|
224
267
|
>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { PluginListenerHandle } from '@capacitor/core'
|
|
2
|
+
import { Keyboard } from '@capacitor/keyboard'
|
|
3
|
+
|
|
4
|
+
// Tracks the on-screen keyboard height on iOS Capacitor only.
|
|
5
|
+
// With Keyboard.resize = 'none', the WebView does not shrink; consumers shift
|
|
6
|
+
// fixed UI (e.g. bottom sheets) with this inset so it moves with the keyboard
|
|
7
|
+
// animation instead of jumping after it finishes. Android keeps soft-input
|
|
8
|
+
// resize, so applying the inset there would double-offset.
|
|
9
|
+
export default function useKeyboardInset() {
|
|
10
|
+
const { isIOS, isPluginAvailable } = useCapacitor()
|
|
11
|
+
const keyboardInset = ref(0)
|
|
12
|
+
|
|
13
|
+
const isKeyboardOpen = computed(() => {
|
|
14
|
+
return keyboardInset.value > 0
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const listenerHandles: PluginListenerHandle[] = []
|
|
18
|
+
|
|
19
|
+
onMounted(async () => {
|
|
20
|
+
if (!import.meta.client || !isIOS.value || !isPluginAvailable('Keyboard')) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
listenerHandles.push(
|
|
25
|
+
await Keyboard.addListener('keyboardWillShow', (info) => {
|
|
26
|
+
keyboardInset.value = info.keyboardHeight
|
|
27
|
+
}),
|
|
28
|
+
await Keyboard.addListener('keyboardWillHide', () => {
|
|
29
|
+
keyboardInset.value = 0
|
|
30
|
+
}),
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
onBeforeUnmount(() => {
|
|
35
|
+
for (const handle of listenerHandles) {
|
|
36
|
+
void handle.remove()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
listenerHandles.length = 0
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
isKeyboardOpen,
|
|
44
|
+
keyboardInset,
|
|
45
|
+
}
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saasmakers/ui",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.80",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Reusable Nuxt UI components for SaaS Makers projects",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@capacitor/core": "8.4.0",
|
|
29
|
+
"@capacitor/keyboard": "8.0.6",
|
|
29
30
|
"@capacitor/preferences": "8.0.1",
|
|
30
31
|
"@capgo/capacitor-social-login": "8.3.22",
|
|
31
32
|
"@nuxt/icon": "2.2.3",
|