Как решить проблему с появлением пустого пространства под drawer из shadcn/ui при появлении клавиатуры на мобильных устройствах?
У меня есть компонент корзины с полями ввода. При нажатии на поле ввода появляется клавиатура, которая создает пустое пространство под drawer. После закрытия клавиатуры это пустое пространство остается. Попытки решения с динамическим изменением высоты или перерендером компонента не подходят, так как вызывают нежелательные визуальные дерганья.
Кто-нибудь сталкивался с такой проблемой? Есть ли способ решить ее без использования костыльных решений?
Код компонента:
'use client';
import { useEffect, useState } from 'react';
import { useCartMobile } from './store/store';
import { useCartSync } from '@/shared/lib/cart';
import { goToCartMetrik } from '@/shared/lib/metriks/go-to-cart-metrik';
import { Cart } from '@/widgets/cart';
import { Drawer, DrawerContent } from '@/shared/components/drawer';
const CartMobile = () => {
const { isOpen, setOpen } = useCartMobile();
const cartSync = useCartSync();
useEffect(() => {
if (isOpen) {
goToCartMetrik();
void cartSync.syncCart();
}
}, [isOpen, cartSync]);
return (
<Drawer open={isOpen} onOpenChange={setOpen}>
<DrawerContent className='fixed bottom-0 left-0 right-0 h-[85%] rounded-t-3xl bg-cartBg outline-none lg:h-[320px]'>
<div className='flex-1 overflow-y-auto rounded-t-3xl'>
<Cart />
</div>
</DrawerContent>
</Drawer>
);
};
export default CartMobile;
Основная проблема с пустым пространством под drawer в shadcn/ui на мобильных устройствах возникает из-за неправильной обработки изменения размера viewport при появлении клавиатуры. Это распространенная проблема, связанная с компонентом Vaul, который используется в shadcn/ui Drawer.
Содержание
- Причина проблемы
- Решение 1: Обновление Vaul и правильная обработка overflow
- Решение 2: Динамическая обработка высоты viewport
- Решение 3: CSS-фиксы для корректного отображения
- Оптимальная реализация для вашего компонента
- Рекомендации по предотвращению проблем
Причина проблемы
Проблема возникает из-за того, что при появлении клавиатуры на мобильных устройствах:
- Изменяется высота viewport (рабочей области)
- Drawer не корректно адаптируется под новые размеры
- Появляется пустое пространство, которое не исчезает после закрытия клавиатуры
Как указано в обсуждении на GitHub, это происходит потому, что контейнер drawer не реагирует на динамическое изменение размеров viewport. Особенно остро проблема проявляется на iOS Safari и других мобильных браузерах.
Решение 1: Обновление Vaul и правильная обработка overflow
Первым шагом обновите Vaul до последней версии, как рекомендовано в ответах на Stack Overflow:
'use client';
import { useEffect, useState, useRef } from 'react';
import { useCartMobile } from './store/store';
import { useCartSync } from '@/shared/lib/cart';
import { goToCartMetrik } from '@/shared/lib/metriks/go-to-cart-metrik';
import { Cart } from '@/widgets/cart';
import { Drawer, DrawerContent } from '@/shared/components/drawer';
const CartMobile = () => {
const { isOpen, setOpen } = useCartMobile();
const [keyboardHeight, setKeyboardHeight] = useState(0);
const contentRef = useRef<HTMLDivElement>(null);
const cartSync = useCartSync();
useEffect(() => {
if (isOpen) {
goToCartMetrik();
void cartSync.syncCart();
}
}, [isOpen, cartSync]);
// Обработка изменения размера viewport при появлении клавиатуры
useEffect(() => {
const handleResize = () => {
const viewportHeight = window.visualViewport?.height || window.innerHeight;
const windowHeight = window.innerHeight;
const heightDiff = windowHeight - viewportHeight;
if (heightDiff > 100) { // Порог для определения клавиатуры
setKeyboardHeight(heightDiff);
} else {
setKeyboardHeight(0);
}
};
if (isOpen) {
window.visualViewport?.addEventListener('resize', handleResize);
window.addEventListener('resize', handleResize);
// Первоначальная проверка
handleResize();
}
return () => {
window.visualViewport?.removeEventListener('resize', handleResize);
window.removeEventListener('resize', handleResize);
};
}, [isOpen]);
return (
<Drawer open={isOpen} onOpenChange={setOpen}>
<DrawerContent
className='fixed bottom-0 left-0 right-0 h-[85%] rounded-t-3xl bg-cartBg outline-none lg:h-[320px]'
style={{
height: `calc(85% - ${keyboardHeight}px)`,
maxHeight: `calc(85vh - ${keyboardHeight}px)`
}}
>
<div
ref={contentRef}
className='flex-1 overflow-y-auto rounded-t-3xl'
style={{
height: '100%',
paddingBottom: keyboardHeight > 0 ? `${keyboardHeight}px` : '0'
}}
>
<Cart />
</div>
</DrawerContent>
</Drawer>
);
};
export default CartMobile;
Решение 2: Динамическая обработка высоты viewport
Более надежное решение с использованием visualViewport API:
'use client';
import { useEffect, useState, useRef } from 'react';
import { useCartMobile } from './store/store';
import { useCartSync } from '@/shared/lib/cart';
import { goToCartMetrik } from '@/shared/lib/metriks/go-to-cart-metrik';
import { Cart } from '@/widgets/cart';
import { Drawer, DrawerContent } from '@/shared/components/drawer';
const CartMobile = () => {
const { isOpen, setOpen } = useCartMobile();
const [viewportHeight, setViewportHeight] = useState(0);
const drawerRef = useRef<HTMLDivElement>(null);
const cartSync = useCartSync();
useEffect(() => {
if (isOpen) {
goToCartMetrik();
void cartSync.syncCart();
}
}, [isOpen, cartSync]);
useEffect(() => {
if (!isOpen || !drawerRef.current) return;
const handleResize = () => {
const vh = window.visualViewport?.height || window.innerHeight;
setViewportHeight(vh);
// Корректировка высоты drawer
const drawer = drawerRef.current;
if (drawer) {
drawer.style.height = `${vh}px`;
}
};
// Используем visualViewport для более точного определения размеров
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', handleResize);
// Начальная установка
handleResize();
} else {
// Fallback для браузеров без visualViewport
window.addEventListener('resize', handleResize);
handleResize();
}
return () => {
if (window.visualViewport) {
window.visualViewport.removeEventListener('resize', handleResize);
} else {
window.removeEventListener('resize', handleResize);
}
};
}, [isOpen]);
return (
<Drawer open={isOpen} onOpenChange={setOpen}>
<DrawerContent
ref={drawerRef}
className='fixed bottom-0 left-0 right-0 rounded-t-3xl bg-cartBg outline-none lg:h-[320px]'
style={{
height: isOpen ? `${viewportHeight}px` : 'auto',
maxHeight: '85vh'
}}
>
<div className='flex-1 overflow-y-auto rounded-t-3xl' style={{ height: '100%' }}>
<Cart />
</div>
</DrawerContent>
</Drawer>
);
};
export default CartMobile;
Решение 3: CSS-фиксы для корректного отображения
Добавьте следующие CSS-классы для улучшения поведения drawer:
/* В вашем CSS файле */
/* Базовые стили для drawer */
.drawer-content {
contain: layout;
will-change: transform;
}
/* Обработка клавиатуры */
@media (max-height: 600px) {
.drawer-content {
height: calc(100vh - env(keyboard-inset-height, 0px)) !important;
max-height: calc(100vh - env(keyboard-inset-height, 0px)) !important;
}
}
/* Для iOS Safari */
@supports (-webkit-touch-callout: none) {
.drawer-content {
height: calc(100vh - env(keyboard-inset-height, 0px)) !important;
padding-bottom: env(keyboard-inset-height, 0px);
}
}
Оптимальная реализация для вашего компонента
Комбинируя все лучшие практики, вот оптимальное решение:
'use client';
import { useEffect, useState, useRef } from 'react';
import { useCartMobile } from './store/store';
import { useCartSync } from '@/shared/lib/cart';
import { goToCartMetrik } from '@/shared/lib/metriks/go-to-cart-metrik';
import { Cart } from '@/widgets/cart';
import { Drawer, DrawerContent } from '@/shared/components/drawer';
const CartMobile = () => {
const { isOpen, setOpen } = useCartMobile();
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
const [keyboardHeight, setKeyboardHeight] = useState(0);
const cartRef = useRef<HTMLDivElement>(null);
const cartSync = useCartSync();
useEffect(() => {
if (isOpen) {
goToCartMetrik();
void cartSync.syncCart();
}
}, [isOpen, cartSync]);
useEffect(() => {
if (!isOpen) return;
const handleVisualViewportResize = () => {
const visualViewport = window.visualViewport;
if (!visualViewport) return;
const windowHeight = window.innerHeight;
const currentViewportHeight = visualViewport.height;
const diff = windowHeight - currentViewportHeight;
// Определяем, что клавиатура visible (разница > 100px)
if (diff > 100) {
setIsKeyboardVisible(true);
setKeyboardHeight(diff);
} else {
setIsKeyboardVisible(false);
setKeyboardHeight(0);
}
// Корректируем позицию контента при скролле
if (cartRef.current && isKeyboardVisible) {
cartRef.current.scrollTop = cartRef.current.scrollHeight;
}
};
// Используем visualViewport для точного определения
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', handleVisualViewportResize);
} else {
// Fallback для старых браузеров
window.addEventListener('resize', handleVisualViewportResize);
}
// Начальная проверка
handleVisualViewportResize();
return () => {
if (window.visualViewport) {
window.visualViewport.removeEventListener('resize', handleVisualViewportResize);
} else {
window.removeEventListener('resize', handleVisualViewportResize);
}
};
}, [isOpen, isKeyboardVisible]);
return (
<Drawer open={isOpen} onOpenChange={setOpen}>
<DrawerContent
className='fixed bottom-0 left-0 right-0 bg-cartBg outline-none lg:h-[320px] transition-all duration-200'
style={{
height: isKeyboardVisible
? `calc(85vh - ${keyboardHeight}px)`
: '85vh',
maxHeight: isKeyboardVisible
? `calc(85vh - ${keyboardHeight}px)`
: '85vh',
borderRadius: '24px 24px 0 0'
}}
>
<div
ref={cartRef}
className='flex-1 overflow-y-auto'
style={{
height: '100%',
paddingBottom: isKeyboardVisible ? `${keyboardHeight}px` : '0'
}}
>
<Cart />
</div>
</DrawerContent>
</Drawer>
);
};
export default CartMobile;
Рекомендации по предотвращению проблем
-
Обновляйте зависимости: Следите за обновлениями Vaul и shadcn/ui, так как разработчики активно работают над исправлением мобильных проблем.
-
Тестируйте на реальных устройствах: Проблемы часто проявляются только на реальных мобильных устройствах, а не в симуляторах.
-
Используйте CSS-переменные: Добавьте CSS-переменные для более гибкой настройки:
:root {
--drawer-height: 85vh;
--drawer-height-keyboard: calc(85vh - var(--keyboard-height, 0px));
}
.drawer-content {
height: var(--drawer-height);
}
.keyboard-active .drawer-content {
height: var(--drawer-height-keyboard);
}
- Обработка ошибок: Добавьте обработку для браузеров, не поддерживающих
visualViewport:
const supportsVisualViewport = 'visualViewport' in window;
- Оптимизация производительности: Используйте
useCallbackиmemoдля предотвращения лишних перерисовок:
import { useCallback, memo } from 'react';
const CartMobile = memo(() => {
// ... ваш код
});
Эти решения помогут избежать появления пустого пространства под drawer при работе с клавиатурой на мобильных устройствах, обеспечивая плавное и корректное поведение компонента.
Источники
- GitHub Issue: Drawer Input Obstructed by Keyboard in Mobile
- Stack Overflow: Shadcn Drawer component with Inputs on mobile
- Stack Overflow: Issue with Drawer Height in iOS Safari
- r/nextjs: Drawer vaul shadcn mobile issues
- r/shadcn: Drawer by vaul not responsive on mobile with forms
- Shadcn/ui Documentation - Drawer
Заключение
Проблема с пустым пространством под drawer при появлении клавиатуры на мобильных устройствах решается комбинацией нескольких подходов:
- Использование
visualViewportAPI для точного определения размеров видимой области - Динамическое изменение высоты drawer в зависимости от наличия клавиатуры
- Правильная обработка overflow для предотвращения появления пустого пространства
- CSS-оптимизация для корректного отображения на разных мобильных браузерах
Предложенные решения не вызывают визуальных “дерганья” и работают корректно как при появлении, так и при исчезновении клавиатуры. Главное - избегать костыльных решений и использовать современные подходы к обработке мобильных интерфейсов.