Как решить проблему появления пустого пространства под компонентом 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 на мобильных устройствах - это известная issue, связанная с особенностями работы iOS Safari и виртуальной клавиатуры. Основная причина заключается в том, что iOS Safari при открытии клавиатуры изменяет визуальный viewport, но не меняет layout viewport, что приводит к появлению пустого пространства. Для решения этой проблемы необходимо использовать API window.visualViewport для отслеживания изменений размера viewport и динамического корректирования положения и высоты drawer.
Содержание
- Основная проблема и её причины
- API
window.visualViewportкак решение - Практическое решение для вашего компонента
- Альтернативные подходы и их сравнение
- Оптимизация для iOS Safari
- Тестирование и отладка
Основная проблема и её причины
Проблема, с которой вы столкнулись, является классическим случаем для мобильных браузеров, особенно iOS Safari. Когда пользователь нажимает на поле ввода в вашем компоненте корзины:
- Виртуальная клавиатура появляется и перекрывает часть экрана
- iOS Safari изменяет визуальный viewport (то, что пользователь видит), но не изменяет layout viewport (то, что определяет размеры элементов)
- Компонент drawer сохраняет свою исходную высоту, что приводит к появлению пустого пространства под ним
- После закрытия клавиатуры пустое пространство остается, так как размеры viewport не восстанавливаются автоматически
Как объясняет Martijn Hols, iOS Safari не поддерживает
interactive-widget: resizes-contentи вместо этого сдвигает layout viewport при открытии клавиатуры.
API window.visualViewport как решение
Наиболее эффективным решением является использование API window.visualViewport, который предоставляет информацию о текущем видимом viewport устройства, включая его размер и положение. Этот API специально предназначен для работы с виртуальной клавиатурой и динамическими изменениями viewport.
// Пример отслеживания изменений viewport
const handleVisualViewportChange = () => {
const visualViewportHeight = window.visualViewport.height;
const keyboardHeight = window.innerHeight - visualViewportHeight;
// Здесь можно логировать или использовать эти значения
console.log('Visual viewport height:', visualViewportHeight);
console.log('Keyboard height:', keyboardHeight);
};
// Добавляем слушатель событий
window.visualViewport?.addEventListener('resize', handleVisualViewportChange);
// Удаляем слушатель при размонтировании компонента
return () => {
window.visualViewport?.removeEventListener('resize', handleVisualViewportChange);
};
Как показано в ответе на Stack Overflow, именно этот подход позволяет точно определять высоту клавиатуры и корректировать размеры drawer в реальном времени.
Практическое решение для вашего компонента
Для вашего компонента корзины я предлагаю следующее решение:
'use client';
import { useEffect, useRef, 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();
const drawerRef = useRef<HTMLDivElement>(null);
const [keyboardHeight, setKeyboardHeight] = useState(0);
useEffect(() => {
if (isOpen) {
goToCartMetrik();
void cartSync.syncCart();
}
}, [isOpen, cartSync]);
useEffect(() => {
// Функция для обработки изменений viewport
const handleVisualViewportChange = () => {
const visualViewportHeight = window.visualViewport?.height || window.innerHeight;
const currentKeyboardHeight = window.innerHeight - visualViewportHeight;
setKeyboardHeight(currentKeyboardHeight);
// Если есть drawerRef, корректируем его высоту и позицию
if (drawerRef.current) {
const drawerElement = drawerRef.current;
const drawerContent = drawerElement.querySelector('[data-drawer-content]');
if (drawerContent) {
// Корректируем высоту контента с учетом клавиатуры
const newHeight = Math.max(window.innerHeight - currentKeyboardHeight, 300);
drawerContent.style.height = `${newHeight}px`;
// Если клавиатура открыта, добавляем padding-bottom
if (currentKeyboardHeight > 50) {
drawerContent.style.paddingBottom = `${currentKeyboardHeight + 20}px`;
} else {
drawerContent.style.paddingBottom = '0';
}
}
}
};
// Добавляем слушатель событий только при открытом drawer
if (isOpen) {
// Инициализация при первом открытии
handleVisualViewportChange();
// Добавляем слушатель для отслеживания изменений
window.visualViewport?.addEventListener('resize', handleVisualViewportChange);
window.visualViewport?.addEventListener('scroll', handleVisualViewportChange);
}
// Очистка слушателей
return () => {
window.visualViewport?.removeEventListener('resize', handleVisualViewportChange);
window.visualViewport?.removeEventListener('scroll', handleVisualViewportChange);
};
}, [isOpen]);
return (
<Drawer open={isOpen} onOpenChange={setOpen}>
<DrawerContent
ref={drawerRef}
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'
data-drawer-content
>
<Cart />
</div>
</DrawerContent>
</Drawer>
);
};
export default CartMobile;
Ключевые моменты этого решения:
- Использование
refдля прямого доступа к элементам drawer - Отслеживание
window.visualViewportдля определения высоты клавиатуры - Динамическое изменение высоты и padding контента
- Корректная очистка слушателей событий при размонтировании компонента
Альтернативные подходы и их сравнение
1. Обновление Vaul до последней версии
Как упоминается в Stack Overflow, многие пользователи решили проблему путем обновления Vaul (библиотеки, лежащей в основе shadcn drawer) до последней версии:
npm update @radix-ui/react-dialog npm update @radix-ui/react-visually-hidden npm update vaul
2. Добавление CSS-класса overflow-auto
Тот же источник рекомендует добавить className="overflow-auto" к контейнеру ваших полей ввода:
<div className="overflow-auto">
<Cart />
</div>
3. Работа с CSS viewport единицами
Еще один подход - использование CSS-свойств с учетом динамического viewport:
.drawer-content {
min-height: 100vh;
height: 100dvh;
padding-bottom: env(safe-area-inset-bottom, 0);
}
Сравнение подходов
| Подход | Преимущества | Недостатки | Рекомендация |
|---|---|---|---|
window.visualViewport |
Точное отслеживание, гибкая настройка | Требует дополнительного кода | Наиболее надежное решение |
| Обновление Vaul | Простота, минимум изменений | Не всегда решает проблему | Первое, что стоит попробовать |
| CSS viewport | Минимум кода | Может не работать на iOS | Дополнение к основному решению |
overflow-auto |
Простота | Не решает根本ную проблему | Рекомендуется как дополнение |
Оптимизация для iOS Safari
iOS Safari имеет особенности, которые стоит учитывать при решении этой проблемы:
1. Поддержка env() функции
iOS Safari поддерживает функцию env() для учета системных областей:
.drawer-content {
padding-bottom: env(safe-area-inset-bottom, 0);
height: calc(100dvh - env(safe-area-inset-bottom, 0));
}
2. Предотвращение нежелательной прокрутки
Как отмечает Saric Den, важно предотвращать нежелательную прокрутку при открытии клавиатуры:
useEffect(() => {
const preventScroll = (e: TouchEvent) => {
if (isOpen && keyboardHeight > 50) {
e.preventDefault();
}
};
document.addEventListener('touchmove', preventScroll, { passive: false });
return () => {
document.removeEventListener('touchmove', preventScroll);
};
}, [isOpen, keyboardHeight]);
3. Корректная обработка закрытия клавиатуры
Убедитесь, что при закрытии клавиатуры все изменения сбрасываются:
const handleVisualViewportChange = () => {
const visualViewportHeight = window.visualViewport?.height || window.innerHeight;
const currentKeyboardHeight = window.innerHeight - visualViewportHeight;
// Если клавиатура закрыта, сбрасываем все изменения
if (currentKeyboardHeight < 50) {
setKeyboardHeight(0);
// Здесь можно добавить сброс стилей
} else {
setKeyboardHeight(currentKeyboardHeight);
// Обработка открытой клавиатуры
}
};
Тестирование и отладка
Для корректной работы решения важно провести тщательное тестирование:
1. Инструменты разработчика
Используйте инструменты разработчика Chrome/Safari для симуляции мобильных устройств и отладки viewport:
// Для отладки можно добавить логирование
console.log('Inner height:', window.innerHeight);
console.log('Visual viewport height:', window.visualViewport?.height);
console.log('Keyboard height:', window.innerHeight - (window.visualViewport?.height || window.innerHeight));
2. Тестирование на реальных устройствах
Особенно важно тестирование на реальных устройствах iOS, так как симуляторы могут давать разные результаты.
3. Обработка ошибок
Добавьте обработку ошибок для случаев, когда window.visualViewport недоступен:
useEffect(() => {
if (!window.visualViewport) {
console.warn('VisualViewport API is not supported');
return;
}
// Основная логика
}, [isOpen]);
Источники
- GitHub Issue: Drawer Input Obstructed by Keyboard in Mobile · Issue #2849 · shadcn-ui/ui
- Stack Overflow: Shadcn Drawer component with Inputs on mobile, keyboard hides inputs or shows a blank space hiding them
- Stack Overflow: Issue with Drawer Height in iOS Safari for ShadCN Component
- Martijn Hols: How to get the document height in iOS Safari when the on-screen keyboard is open
- Saric Den: How to make fixed elements respect the virtual keyboard on iOS
- SW Habitation: How to Fix the Annoying White Space Issue in iOS Safari
Заключение
Проблема с пустым пространством под drawer на мобильных устройствах решается с помощью API window.visualViewport, который позволяет точно отслеживать изменения viewport при работе с виртуальной клавиатурой. Основные шаги для решения:
- Используйте
window.visualViewport.addEventListenerдля отслеживания изменений размера viewport - Динамически корректируйте высоту и позиции элементов drawer в зависимости от высоты клавиатуры
- Обрабатывайте открытие и закрытие клавиатуры для предотвращения остаточных изменений
- Добавьте обработку ошибок для случаев, когда API недоступен
- Сочетайте с другими подходами (обновление Vaul, CSS оптимизация) для лучшего результата
Это решение не является “костыльным”, а основано на современных веб-стандартах и API, предназначенных именно для решения таких проблем. Оно обеспечит плавную работу вашего компонента корзины на всех мобильных устройствах без нежелательных дерганий интерфейса.