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

refactor: ModalRoot/ModalPage/ModalCard #6759

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

inomdzhon
Copy link
Contributor

@inomdzhon inomdzhon commented Mar 26, 2024

Проверить после


  • Unit-тесты
  • e2e-тесты
  • Дизайн-ревью
  • Документация фичи
  • Release notes

Описание

Переписаны компоненты ModalPage/ModalCard

⚠️ Теперь компоненты могут использоваться без 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().

Переписан компонент ModalRoot

⚠️ Теперь лишь отвечает за состояние 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.

Изменения, которые нужно вынести в отдельные PR после первичного ревью

Нюансы

Обратная совместимость

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

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

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>
);

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

BottomSheet, анимации и свайп

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

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

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

dynamicContentHeight

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

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

Адативность

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

Решения

Выделение текста

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

Вертикальный и горизонтальный скроллы

При touchstart достаём скроллируемый элемент через event.target и подписываемся на событие скролла, а также получаем информацию про позицию скролла.

  • вертикальный скролл: проверяем его наличие и положение scrollTop – отключаем взаимодействие если scrollTop !== 0;
  • горизонтальный скролл: проверяем его наличие и направление жеста – отключаем если жест идёт по оси X.

Плавающие элементы внутри модалки

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

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

Поля ввода

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

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

Так как иные решения приводят к другим проблемам (подробнее можно прочесть в JSDoc хука useVirtualKeyboardState()), следующие баги нужно закрыть:

Референсы

Copy link
Contributor

github-actions bot commented Mar 26, 2024

size-limit report 📦

Path Size
JS 382.79 KB (-0.07% 🔽)
JS (gzip) 122.25 KB (-0.25% 🔽)
JS (brotli) 101.3 KB (-0.23% 🔽)
JS import Div (tree shaking) 1.47 KB (0%)
CSS 344.04 KB (+0.54% 🔺)
CSS (gzip) 49.64 KB (+0.66% 🔺)
CSS (brotli) 40.18 KB (+0.58% 🔺)

Copy link

codesandbox-ci bot commented Mar 26, 2024

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit bce0326:

Sandbox Source
vkui boilerplate (forked) Issue #1494

Copy link
Contributor

github-actions bot commented Mar 26, 2024

e2e tests

Playwright Report

Copy link
Contributor

github-actions bot commented Mar 26, 2024

👀 Docs deployed

Commit bce0326

Copy link

codecov bot commented Mar 26, 2024

Codecov Report

Attention: Patch coverage is 64.13570% with 222 lines in your changes missing coverage. Please review.

Project coverage is 93.65%. Comparing base (e985501) to head (bce0326).
Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
...src/lib/sheet/controllers/BottomSheetController.ts 28.32% 124 Missing ⚠️
packages/vkui/src/hooks/useVirtualKeyboardState.ts 62.02% 30 Missing ⚠️
...c/lib/sheet/controllers/CSSTransitionController.ts 32.25% 21 Missing ⚠️
...kui/src/components/ModalCard/ModalCardInternal.tsx 82.35% 9 Missing ⚠️
...kui/src/components/ModalPage/ModalPageInternal.tsx 87.50% 9 Missing ⚠️
packages/vkui/src/lib/dom.tsx 67.85% 9 Missing ⚠️
...src/components/ModalPageFooter/ModalPageFooter.tsx 0.00% 5 Missing ⚠️
packages/vkui/src/lib/sheet/useBottomSheet.ts 88.09% 5 Missing ⚠️
...kages/vkui/src/lib/touch/UIPanGestureRecognizer.ts 28.57% 5 Missing ⚠️
packages/vkui/src/lib/adaptivity/functions.ts 60.00% 2 Missing ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6759      +/-   ##
==========================================
- Coverage   95.34%   93.65%   -1.69%     
==========================================
  Files         377      386       +9     
  Lines       11054    11050       -4     
  Branches     3673     3631      -42     
==========================================
- Hits        10539    10349     -190     
- Misses        515      701     +186     
Flag Coverage Δ
unittests 93.65% <64.13%> (-1.69%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@vkcom-publisher vkcom-publisher added pr-needs-work Автоматизация: PR автоматически закроется через 14 дней при отсутствии активности and removed pr-needs-work Автоматизация: PR автоматически закроется через 14 дней при отсутствии активности labels Apr 3, 2024
@inomdzhon inomdzhon added the no-stale Добавляет PR в исключения для автоматического закрытия label Apr 6, 2024
@inomdzhon inomdzhon force-pushed the imirdzhamolov/research/refactor-modal branch 3 times, most recently from 4a0d455 to 7d03282 Compare May 16, 2024 16:03
@inomdzhon inomdzhon force-pushed the imirdzhamolov/research/refactor-modal branch 2 times, most recently from cd0da9c to c382b8f Compare August 1, 2024 14:58
@inomdzhon inomdzhon force-pushed the imirdzhamolov/research/refactor-modal branch from c382b8f to fba65d5 Compare September 30, 2024 09:04
@inomdzhon inomdzhon force-pushed the imirdzhamolov/research/refactor-modal branch 3 times, most recently from c1453c5 to a1c9447 Compare October 28, 2024 23:33
@inomdzhon inomdzhon changed the title [Draft] feat: create ModalPageV2 refactor: ModalRoot/ModalPage/ModalCard Oct 28, 2024
@inomdzhon inomdzhon force-pushed the imirdzhamolov/research/refactor-modal branch 6 times, most recently from 481fc96 to f07b8b4 Compare October 30, 2024 15:25
@inomdzhon inomdzhon marked this pull request as ready for review October 30, 2024 15:25
@inomdzhon inomdzhon requested a review from a team as a code owner October 30, 2024 15:25
Copy link
Contributor

@andrey-medvedev-vk andrey-medvedev-vk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Грандиозная работа! 👏 👏 👏 💪

Здорово раскидал по компонентам!
Понравилось, что компоненты теперь функции как практически везде!
Офигенно как переведена анимация/перетаскивание на CSS Transition и контроллеры. (Там я, конечно, поплыл, но приятно видеть знакомое API с коллбэками и то, что всё сводится к установке css переменных)


Надо пристально пройтись по местам где коллбёки передаются в компоненты, а не напрямую в html элементы и обернуть их useCallback, и по местам, где объекты возвращаются из хуков или передаются в контекст, чтобы обернуть в useMemo и избежать лишних ререндеров.


По стайлгайду и сторибуку прошелся и так и на устройстве, ничего такого не заметил.

packages/vkui/src/components/ModalRoot/ModalRoot.tsx Outdated Show resolved Hide resolved
const context = useContext(ModalRootContext);
const opened = context.isInsideModal ? context.activeModal === id : open;

const [unmounted, setUnmounted] = useState(keepMounted ? false : !opened);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:
А почему решил именно с unmounted работать?
Немного тяжело читается, приходится инверсировать в голове, когда unmounted: false, вместо mounted: true.

Вижу плюс в том, что ниже условия при которых надо раньше выйти из рендер функции компонента пишутся без отрицания, типа if unmounted (вместо if !mounted или if mounted === false.
Но, честно говоря, явное if mounted === false читается проще.

Ещё пара легковесных аргументов за mounted.

  • нас уже есть проп keepMounted
  • в FocusTrap используется mounted. Мы его в этом PR трогаем и немного не консистентно смотрится.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

хороший поинт 👍

поправил, но с одним нюансом – в API получаем mounted, а внутри оставил реализацию через unmounted, а то у мне не получилось переписать useEffect() так, чтобы это работало наоборот x)))

packages/vkui/src/hooks/useFocusTrap.ts Outdated Show resolved Hide resolved
packages/vkui/src/lib/dom.tsx Outdated Show resolved Hide resolved
@inomdzhon
Copy link
Contributor Author

Надо пристально пройтись по местам где коллбёки передаются в компоненты, а не напрямую в html элементы и обернуть их useCallback, и по местам, где объекты возвращаются из хуков или передаются в контекст, чтобы обернуть в useMemo и избежать лишних ререндеров.

добавил мемоизацию там где можно, но во всех местах сильная завязка на пользователя, т.к. используются колбеки (onOpen, onOpened, onClose, onClosed), которые пользователь сам должен мемоизировать

ждём React 19, где не нужно будет руками мемоизировать)

@inomdzhon inomdzhon mentioned this pull request Nov 1, 2024
1 task
@inomdzhon inomdzhon force-pushed the imirdzhamolov/research/refactor-modal branch from cb1bd66 to 9473173 Compare November 2, 2024 14:46
inomdzhon added a commit that referenced this pull request Nov 5, 2024
- `CustomSelect` жалуется на `renderOption` в 729 строке, что якобы идёт обращение к `ref.current`, но такого там нет;
    ```plain
    729:51  error  Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)  react-compiler/react-compiler
    ```
- `ModalRootContext` и `useModalManager` переписываются в PR #6759.
@inomdzhon inomdzhon force-pushed the imirdzhamolov/research/refactor-modal branch from 9473173 to bce0326 Compare November 5, 2024 11:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment