Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Не скролится модалка пока открыта клавиатура #599

Closed
mahnunchik opened this issue Mar 24, 2020 · 2 comments
Assignees
Milestone

Comments

@mahnunchik
Copy link

Шаги воспроизведения:

  1. Открыть длинную модалку с инпутом (search).
  2. Открыть клавиатуру.
  3. Попытаться проскроллить модалку.

Фактический результат: Модалка не скроллится. до тех пор, пока мы не закроем клавиатуру.

Ожидаемый результат: Модалка скроллится.

Скринкаст:

Оригинальный тикет:

@mahnunchik
Copy link
Author

IMG_2463.mp4.zip
IMG_2464.mp4.zip

Перезалил скринкаст проблемы. Доступ к трекеру продукта закрыт от всех, а ссылка на видео протухла.

@SevereCloud SevereCloud moved this to Backlog in VKUI Aug 1, 2022
@SevereCloud SevereCloud added this to VKUI Aug 1, 2022
@SevereCloud SevereCloud self-assigned this Jun 26, 2023
@SevereCloud SevereCloud removed their assignment Jul 28, 2024
inomdzhon added a commit that referenced this issue Dec 2, 2024
h2. Описание

<details><summary>Переписаны компоненты ModalPage/ModalCard</summary>
<p>

⚠️ Теперь компоненты могут использоваться без `ModalRoot`.

`ModalPage` / `ModalCard`:

- добавлено свойство `open`;
- добавлено свойство `keepMounted`;
- типа `onClose` изменён с `VoidFunction` на `(reason: ModalPageCloseReason, event?: UIEvent<HTMLElement>) => void`;
- добавлено свойство `noFocusToDialog`, приоритетней чем `noFocusToDialog` в `ModalRoot`;
- добавлено свойство `modalOverlayTestId`, приоритетней чем `modalOverlayTestId` в `ModalRoot`.
- создан компонент `ModalOutlet`;
- создан компонент `ModalOverlay`;
- основной код работы модалок вынесен в отдельные компоненты `ModalPageInternal` и `ModalCardInternal`, у них есть свойство `ModalOverlay`;
- создан контекст `ModalContext`, который теперь используется в компонентах `ModalPageHeader`, `Group` и `PanelHeader` вместо `ModalRootContext`.

`ModalPage`:

- `settlingHeight` теперь имеет значение по умолчанию `50%` вместо `75%`, обратил внимание, что в обычно `BottomSheet`'ы открываются на половину экрана;
- добавлено свойство `footer`, а также создан компонент `ModalPageFooter`;
- созданы компонент `ModalPageContent` – вынес, чтобы можно было в будущем перейти на сбор `ModalPage` через композицию компонентов.

`lib/sheet`:

В папке хранится логика отвечающая за взаимодействие с модалкой на мобильных экранах. Отдаёт хук `useBottomSheet()`.

</p>
</details>

<details><summary>Переписан компонент ModalRoot</summary>
<p>

⚠️ Теперь лишь отвечает за состояние `open` компонентов `ModalCard` и `ModalPage` в зависимости от их `id`/`nav` и параметра `activeModal`, а также рендера общей `ModalOverlay` для всех модалок.

`ModalRoot`:

- добавлены события `onOpen`, `onOpened`, `onClosed`, которые всплывают от `activeModal`;
- в `PopoutRoot` удалён `PopoutRootModal` в пользу `ModalOutlet` у `ModalPage` и `ModalCard`.

`ModalRootContext`:

- **⚠️ BREAKING CHANGE** в свойство `onClose` нужно теперь обязательно передавать `id` модального окна;
- добавлены события `onOpen`, `onOpened`, `onClosed`, чтобы их могли вызывать `ModalPage` и `ModalCard`;
- свойство `registerModal` теперь `@deprecated` – не нужно отдельно регистрировать модальное окно.
- свойство `updateModalHeight` теперь `@deprecated` – задача с обновлением высоты контента при `dynamicContentHeight` решается через CSS;

`ModalRootOverlayContext` / `VisuallyHiddenModalOverlay`:

- чтобы создать общий `ModalOverlay` для всех модалок в контексте `ModalRoot`, происходит подмена `ModalOverlay` в `ModalPage` и `ModalCard` на `VisuallyHiddenModalOverlay`, который отвечает за приём `onClick` и `modalOverlayTestId`, а сам `ModalOverlay` попадает в начало `ModalRoot`

`useModalManager()`:

- отвечает за `unmounted` состояние;
- отвечает за регулирования приоритета параметров из `ModalRoot` и `ModalPage`/`ModalCard`;
- отвечает за подмену `ModalOverlay` на `VisuallyHiddenModalOverlay`.

`withModalRootContext`:

- ввиду отказа от `updateModalHeight` HOC  тоже `@deprecated`.

</p>
</details> 

h2. Нюансы

<details><summary>Обратная совместимость</summary>
<p>

Постарался сделать так, чтобы миграция прошла бесследно.

Сломаются вот такой кейс:

```tsx
const MyModal = ({ id }) => { // пропустили settinglingHeight / dynamicContentHeight
  return <ModalPage>Lorem Ipsum</ModalPage>
};

const App = () => (
  <ModalRoot>
    <MyModal
      id="example-1"
      settinglingHeight={100} // устанавливалось здесь, т.к. раньше ModalRoot итерировал по потомкам и доставал этот параметр
    />
    
    <MyModal
      id="example-2"
      dynamicContentHeight // устанавливалось здесь, т.к. раньше ModalRoot итерировал по потомкам и доставал этот параметр
    />
  </ModalRoot>
);
```

который нужно будет править руками.

</p>
</details> 

<details><summary>BottomSheet, анимации и свайп</summary>
<p>

Полное появление и полное скрытие происходит через `transform`, но анимация взаимодействия через свайп реализована через `height`, т.к. это оказалось самым оптимальным способом для решения задач:

- закреплённый `ModalPageFooter` внизу;
- возможность скроллить при `settlingHeight` меньше `100`;
- обновление высоты, если задан `dynamicContentHeight`.

Нашёл решение допустимым, т.к. свайп используется либо для закрытия, либо для разворачивания/сворачивания модального окна на всю или на половину страницы. В первом случае сработает закрытие через `transform`, а во втором разворачивание/сворачивание произойдёт через `height`.

</p>
</details>

<details><summary>dynamicContentHeight</summary>
<p>

> см. предыдущий пункт про **BottomSheet** для контекста

Обновление высоты происходит без анимации, т.к. `height: auto` не анимируется. Опустил анимирование, т.к. усложняет компонент. В теории можно прибегнуть к `useResizeObserver()`.

</p>
</details> 

<details><summary>Адативность</summary>
<p>

При `platform="vkcom"` и при разрешении экрана `767px` компонент теперь превращается в **BottomSheet**, но при этом логику взаимодействия через тач не имеет, т.к. `isDesktop` при `platform="vkcom"` всегда `true` вне зависимости от размера экрана.

</p>
</details> 

h2. Решения

<details><summary>Выделение текста</summary>
<p>

С помощью функции `hasSelectionWithRangeType` определяем, что пользователь выделил текст и перестаём реагировать на `touchstart` и `touchmove` пока выделение не будет удалено.

</p>
</details> 

<details><summary>Вертикальный и горизонтальный скроллы</summary>
<p>

- **вертикальный скролл:**:
  - **основной скролл (`ModalPageContent`)**: проверяем на `scrollTop !== 0`
  - **другие скроллы**: при `touchstart` достаём скроллируемый элемент через `event.target` и проверяем на положение `scrollTop !== 0` и направление пальца вверх
- **горизонтальный скролл:** не блочится, т.к. реагируем на события только по оси Y.

</p>
</details>

<details><summary>Плавающие элементы внутри модалки</summary>
<p>

Нужно рекомендовать использовать `forcePortal` – в коде проверяем, что идёт взаимодействие с элементом вне модалки.

Или нужно рекомендовать добавлять в корневой элемент плавающего элемента атрибут `data-vkui-prevent-swipe` .

</p>
</details>

<details><summary>Поля ввода</summary>
<p>

Наилучшего варианта не нашёл кроме как:

1. через `useVirtualKeyboardState()` узнавать, что пользователь работает с клавиатурой, и перебивать `safe-area-inset-bottom` на ~тот, что возвращает хук~ **0** (попытка вычислять разницу высоты через `VisualViewport`, чтобы реагировать на смену размера клавиатуры, например, из-за панели эмодзи, не удалась);
2. в том же хуке слушать событие скролла на `window` и сохранять его позицию на `window.scrollTo(0, visualViewport.offsetTop)`.

Так как иные решения приводят к другим проблемам (подробнее можно прочесть в **JSDoc** хука `useVirtualKeyboardState()`), следующие баги нужно закрыть:
- [#338](https://github.com/VKCOM/VKUI/issues/338)
- [#599](https://github.com/VKCOM/VKUI/issues/599)

</p>
</details> 

h2. Референсы

- [Youtube • Универсальные попапы или UIKit против / Антон Спивак](https://www.youtube.com/watch?v=jQC_jxtf500)
- [Habr • Bottom sheet: Scrolling and interactions](https://habr.com/ru/companies/koshelek/articles/703260/)
- [How to present a Bottom Sheet in iOS 15 with UISheetPresentationController](https://sarunw.com/posts/bottom-sheet-in-ios-15-with-uisheetpresentationcontroller/)
- [GitHub • ionic-framework/core/src/components/modal](https://github.com/ionic-team/ionic-framework/tree/main/core/src/components/modal)
- [GitHub • stipsan/react-spring-bottom-sheet](https://github.com/stipsan/react-spring-bottom-sheet)
- [GitHub • gorhom/react-native-bottom-sheet](https://github.com/gorhom/react-native-bottom-sheet)
- [GitHub • stanleyugwu/react-native-bottom-sheet](https://github.com/stanleyugwu/react-native-bottom-sheet)
- [GitHub • Temzasse/react-modal-sheet](https://github.com/Temzasse/react-modal-sheet)
- [GitHub • tech-systems/panes](https://github.com/tech-systems/panes)
- [#5092](https://github.com/VKCOM/VKUI/issues/5092) – Пример рефактора модалок с помощью touch-событий

h2. Release notes
h2. BREAKING CHANGE
- ModalRoot: удалена реализация контекста через `React.cloneElement`, теперь не нужно задавать `settlingHeight` и `dynamicContentHeight` на обёртку над `ModalPage` / `ModalCard`
  <details>
  <summary>Пример миграции 1</summary>
  
  ```diff
  const SomeWrapper = ({ id }) => (
    <Modal
      id={id}
  +  settlingHeight={100}
    />
  );

  <ModalRoot activeModal="m">
    <SomeWrapper
      id="m"
  -  settlingHeight={100}
    />
  </ModalRoot>
  ```
  </details>
  
  <details>
  <summary>Пример миграции 2</summary>
  
  ```diff
  - const SomeWrapper = ({ id }) => (
  + const SomeWrapper = (props) => (
    <Modal
  -  id={id}
  +  {...props}
    />
  );

  <ModalRoot activeModal="m">
    <SomeWrapper
      id="m"
      settlingHeight={100}
    />
  </ModalRoot>
  ```
  </details>
h2. Улучшения
- ModalRoot:
  - `updateModalHeight()` помечен как `@depreacted`, т.к. в нём больше нет необходимости – `ModalPage`, при `dynamicContentHeight`, теперь автоматически подстраиваются под контент;
  - `registerModal()` помечен как `@depreacted`, т.к. изменилась логика работы компонента – теперь `ModalPage` и `ModalCard` ориентируется на контекст, создаваемый `ModalRoot`;
  - добавлено свойство `usePortal`.
- `ModalPage`:
  - теперь можно использовать без `ModalRoot` (для рендера в портале можно обернуть в `AppRootPortal`);
  - изменилось значение по умолчанию у свойств `settlingHeight` – `75` → `50` ;
  - добавлено свойство `keepMounted`;
  - добавлено свойство `footer`;
  - добавлено свойство `disableContentPanningGestureProp`;
  - расширен тип `onClose` до `(reason: ModalPageCloseReason, event?: UIEvent<HTMLElement>) => void`.
- `ModalCard`:
  - теперь можно использовать без `ModalRoot` (для рендера в портале можно обернуть в `AppRootPortal`);
  - добавлено свойство `keepMounted`;
  - расширен тип `onClose` до `(reason: ModalPageCloseReason, event?: UIEvent<HTMLElement>) => void`.
@inomdzhon inomdzhon self-assigned this Dec 3, 2024
@inomdzhon
Copy link
Contributor

inomdzhon commented Dec 3, 2024

Частично исправилось в v7.0.0 (#6759) – можно скроллить сам контент. Когда открывается клавиатура, отключается возможность закрывать свайпом модалку.

@github-project-automation github-project-automation bot moved this from 🗃 Backlog to ✅ Done in VKUI Dec 3, 2024
@inomdzhon inomdzhon added this to the v7.0.0 milestone Dec 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

No branches or pull requests

5 participants