НейроАгент

Как убрать пустое пространство под drawer на мобильных устройствах

Решение проблемы пустого пространства под shadcn/ui drawer на iOS Safari. Использование window.visualViewport API для корректировки высоты при появлении клавиатуры.

Вопрос

Как решить проблему появления пустого пространства под компонентом drawer из shadcn/ui на мобильных устройствах?

У меня есть компонент корзины, в котором есть поля ввода. При нажатии на поле ввода появляется клавиатура, что создает пустое пространство под drawer. После закрытия клавиатуры это пустое пространство остается. Решения с динамическим изменением высоты или перерендером компонента не подходят, так как вызывают нежелательные дерганья интерфейса.

Сталкивались ли вы с такой проблемой и есть ли какой-то способ ее решить без использования костыльных решений?

Код компонента:

jsx
'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.

Содержание

Основная проблема и её причины

Проблема, с которой вы столкнулись, является классическим случаем для мобильных браузеров, особенно iOS Safari. Когда пользователь нажимает на поле ввода в вашем компоненте корзины:

  1. Виртуальная клавиатура появляется и перекрывает часть экрана
  2. iOS Safari изменяет визуальный viewport (то, что пользователь видит), но не изменяет layout viewport (то, что определяет размеры элементов)
  3. Компонент drawer сохраняет свою исходную высоту, что приводит к появлению пустого пространства под ним
  4. После закрытия клавиатуры пустое пространство остается, так как размеры viewport не восстанавливаются автоматически

Как объясняет Martijn Hols, iOS Safari не поддерживает interactive-widget: resizes-content и вместо этого сдвигает layout viewport при открытии клавиатуры.

API window.visualViewport как решение

Наиболее эффективным решением является использование API window.visualViewport, который предоставляет информацию о текущем видимом viewport устройства, включая его размер и положение. Этот API специально предназначен для работы с виртуальной клавиатурой и динамическими изменениями viewport.

javascript
// Пример отслеживания изменений 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 в реальном времени.

Практическое решение для вашего компонента

Для вашего компонента корзины я предлагаю следующее решение:

jsx
'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;

Ключевые моменты этого решения:

  1. Использование ref для прямого доступа к элементам drawer
  2. Отслеживание window.visualViewport для определения высоты клавиатуры
  3. Динамическое изменение высоты и padding контента
  4. Корректная очистка слушателей событий при размонтировании компонента

Альтернативные подходы и их сравнение

1. Обновление Vaul до последней версии

Как упоминается в Stack Overflow, многие пользователи решили проблему путем обновления Vaul (библиотеки, лежащей в основе shadcn drawer) до последней версии:

bash
npm update @radix-ui/react-dialog
npm update @radix-ui/react-visually-hidden
npm update vaul

2. Добавление CSS-класса overflow-auto

Тот же источник рекомендует добавить className="overflow-auto" к контейнеру ваших полей ввода:

jsx
<div className="overflow-auto">
  <Cart />
</div>

3. Работа с CSS viewport единицами

Еще один подход - использование CSS-свойств с учетом динамического viewport:

css
.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() для учета системных областей:

css
.drawer-content {
  padding-bottom: env(safe-area-inset-bottom, 0);
  height: calc(100dvh - env(safe-area-inset-bottom, 0));
}

2. Предотвращение нежелательной прокрутки

Как отмечает Saric Den, важно предотвращать нежелательную прокрутку при открытии клавиатуры:

javascript
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. Корректная обработка закрытия клавиатуры

Убедитесь, что при закрытии клавиатуры все изменения сбрасываются:

javascript
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:

javascript
// Для отладки можно добавить логирование
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 недоступен:

javascript
useEffect(() => {
  if (!window.visualViewport) {
    console.warn('VisualViewport API is not supported');
    return;
  }

  // Основная логика
}, [isOpen]);

Источники

  1. GitHub Issue: Drawer Input Obstructed by Keyboard in Mobile · Issue #2849 · shadcn-ui/ui
  2. Stack Overflow: Shadcn Drawer component with Inputs on mobile, keyboard hides inputs or shows a blank space hiding them
  3. Stack Overflow: Issue with Drawer Height in iOS Safari for ShadCN Component
  4. Martijn Hols: How to get the document height in iOS Safari when the on-screen keyboard is open
  5. Saric Den: How to make fixed elements respect the virtual keyboard on iOS
  6. SW Habitation: How to Fix the Annoying White Space Issue in iOS Safari

Заключение

Проблема с пустым пространством под drawer на мобильных устройствах решается с помощью API window.visualViewport, который позволяет точно отслеживать изменения viewport при работе с виртуальной клавиатурой. Основные шаги для решения:

  1. Используйте window.visualViewport.addEventListener для отслеживания изменений размера viewport
  2. Динамически корректируйте высоту и позиции элементов drawer в зависимости от высоты клавиатуры
  3. Обрабатывайте открытие и закрытие клавиатуры для предотвращения остаточных изменений
  4. Добавьте обработку ошибок для случаев, когда API недоступен
  5. Сочетайте с другими подходами (обновление Vaul, CSS оптимизация) для лучшего результата

Это решение не является “костыльным”, а основано на современных веб-стандартах и API, предназначенных именно для решения таких проблем. Оно обеспечит плавную работу вашего компонента корзины на всех мобильных устройствах без нежелательных дерганий интерфейса.