Skip to content

Commit

Permalink
Always render ModalRoot through portal after content
Browse files Browse the repository at this point in the history
We keep render ModalRoot in the SplitLayout container right
after content, but now we use portal for it.
It means now, that users can put ModalRoot anywhere in the app,
they are not limited by SplitLayout modal prop

Styles from PopoutRoot about modal fixed layout and
z-index are moved directly to ModalRoot AppPortalWrapper from
SplitLayout
  • Loading branch information
andrey-medvedev-vk committed Oct 11, 2024
1 parent 2eced1a commit 923d880
Show file tree
Hide file tree
Showing 7 changed files with 40 additions and 26 deletions.
2 changes: 2 additions & 0 deletions packages/vkui/src/components/AppRoot/AppRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export const AppRoot = ({
...props
}: AppRootProps): React.ReactNode => {
const appRootRef = React.useRef<HTMLDivElement | null>(null);
const popoutModalContainerRef = React.useRef<HTMLDivElement | null>(null);
const portalRootRef = React.useRef<HTMLElement | null>(
portalRootProp ? extractPortalRootByProp(portalRootProp) : null,
);
Expand Down Expand Up @@ -121,6 +122,7 @@ export const AppRoot = ({
() => ({
appRoot: appRootRef,
portalRoot: portalRootRef,
popoutModalRoot: popoutModalContainerRef,
setPortalRoot: (element: HTMLElement) => (portalRootRef.current = element),
safeAreaInsets,
embedded: mode === 'embedded',
Expand Down
2 changes: 2 additions & 0 deletions packages/vkui/src/components/AppRoot/AppRootContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { type AppRootUserSelectMode, type SafeAreaInsets } from './types';
export interface AppRootContextInterface {
appRoot: React.RefObject<HTMLElement>;
portalRoot: React.MutableRefObject<HTMLElement | null>;
popoutModalRoot: React.MutableRefObject<HTMLDivElement | null>;
setPortalRoot: (element: HTMLElement) => void;
safeAreaInsets?: SafeAreaInsets;
embedded: boolean;
Expand All @@ -24,6 +25,7 @@ export const DEFAULT_APP_ROOT_CONTEXT_VALUE: AppRootContextInterface = {
appRoot: React.createRef(),
mode: 'full',
portalRoot: React.createRef(),
popoutModalRoot: React.createRef(),
setPortalRoot: noop,
safeAreaInsets: undefined,
embedded: false,
Expand Down
28 changes: 17 additions & 11 deletions packages/vkui/src/components/AppRoot/AppRootPortal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,20 @@ export interface AppRootPortalProps extends HasChildren {
* - При передаче `true` будет использовать `portalRoot` из контекста `AppRoot`.
* - При передаче элемента будут игнорироваться `portalRoot` и `disablePortal` из контекста `AppRoot`.
*/
usePortal?: boolean | HTMLElement | React.RefObject<HTMLElement> | null;
usePortal?: boolean | HTMLElement | React.RefObject<HTMLElement> | null | 'in-app-after-content';
className?: string;
}

export const AppRootPortal = ({ children, usePortal }: AppRootPortalProps): React.ReactNode => {
const { portalRoot, setPortalRoot, appRoot, mode, disablePortal } =
React.useContext(AppRootContext);
export const AppRootPortal = ({
children,
usePortal,
className,
}: AppRootPortalProps): React.ReactNode => {
const { setPortalRoot, appRoot, mode, disablePortal } = React.useContext(AppRootContext);
const appearance = useAppearance();

const canUsePortal = shouldUsePortal(usePortal, mode, Boolean(disablePortal));
const portalContainer = resolvePortalContainer(usePortal, portalRoot.current);
const portalContainer = usePortalContainer(usePortal);

useIsomorphicLayoutEffect(
// Создаём контейнер для портала по запросу один раз
Expand All @@ -50,7 +54,7 @@ export const AppRootPortal = ({ children, usePortal }: AppRootPortalProps): Reac
if (canUsePortal && portalContainer) {
return createPortal(
<AppearanceProvider value={appearance}>
<AppRootStyleContainer>{children}</AppRootStyleContainer>
<AppRootStyleContainer className={className}>{children}</AppRootStyleContainer>
</AppearanceProvider>,
portalContainer,
);
Expand All @@ -75,12 +79,14 @@ function shouldUsePortal(
return disablePortal === false && usePortal === true;
}

function resolvePortalContainer<PortalRootFromContext extends HTMLElement | null | undefined>(
usePortal: AppRootPortalProps['usePortal'],
portalRootFromContext: PortalRootFromContext,
): HTMLElement | null {
function usePortalContainer(usePortal: AppRootPortalProps['usePortal']): HTMLElement | null {
const { portalRoot, popoutModalRoot } = React.useContext(AppRootContext);
if (typeof usePortal === 'boolean' || usePortal === undefined) {
return portalRootFromContext ? portalRootFromContext : null;
return portalRoot.current;
}

if (usePortal === 'in-app-after-content') {
return popoutModalRoot.current;
}

return isRefObject(usePortal) ? usePortal.current : usePortal;
Expand Down
9 changes: 9 additions & 0 deletions packages/vkui/src/components/ModalRoot/ModalRoot.module.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
.host {
position: fixed;
inset-inline-start: 0;
inset-block-start: 0;
z-index: var(--vkui--z_index_modal);
inline-size: 100%;
block-size: 100%;
}

.wrapper {
inline-size: 100%;
block-size: 100%;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/vkui/src/components/ModalRoot/ModalRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -545,12 +545,12 @@ class ModalRootTouchComponent extends React.Component<
}

return (
<AppRootPortal>
<AppRootPortal className={styles.host} usePortal="in-app-after-content">
<TouchRootContext.Provider value={true}>
<ModalRootContext.Provider value={this.modalRootContext}>
<Touch
className={classNames(
styles.host,
styles.wrapper,
this.props.configProvider?.hasCustomPanelHeaderAfter &&
styles.hasCustomPanelHeaderAfterSlot,
touchDown && classNames(styles.touched, 'vkuiInternalModalRoot--touched'),
Expand Down
12 changes: 1 addition & 11 deletions packages/vkui/src/components/PopoutRoot/PopoutRoot.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,6 @@
block-size: 100%;
}

.modal {
position: fixed;
inset-inline-start: 0;
inset-block-start: 0;
z-index: var(--vkui--z_index_modal);
inline-size: 100%;
block-size: 100%;
}

.popout:empty,
.modal:empty {
.popout:empty {
display: none;
}
9 changes: 7 additions & 2 deletions packages/vkui/src/components/PopoutRoot/PopoutRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
'use client';

import * as React from 'react';
import { classNames } from '@vkontakte/vkjs';
import type { HTMLAttributesWithRootRef } from '../../types';
import { AppRootContext } from '../AppRoot/AppRootContext';
import { RootComponent } from '../RootComponent/RootComponent';
import styles from './PopoutRoot.module.css';

Expand Down Expand Up @@ -38,12 +41,14 @@ export const PopoutRoot = ({
children,
...restProps
}: PopoutRootProps): React.ReactNode => {
const { popoutModalRoot } = React.useContext(AppRootContext);

return (
<RootComponent {...restProps} baseClassName={styles.host}>
{children}
<div>
<div ref={popoutModalRoot}>
{!!popout && <PopoutRootPopout>{popout}</PopoutRootPopout>}
{!!modal && <PopoutRootModal>{modal}</PopoutRootModal>}
{modal}
</div>
</RootComponent>
);
Expand Down

0 comments on commit 923d880

Please sign in to comment.