@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="onAfterLeave"
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 safe-bottom dark:bg-gray-900 dark:text-gray-100"
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-200 ease motion-reduce:transition-none motion-reduce:duration-0': !drag.isDragging && (drag.closing || drag.offset > 0),
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.78",
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",