Skip to content

Commit

Permalink
fix(plasma-core): refactor in PopupBase/ModalBase
Browse files Browse the repository at this point in the history
  • Loading branch information
kayman233 committed Sep 29, 2023
1 parent fe9656e commit e052d20
Show file tree
Hide file tree
Showing 14 changed files with 182 additions and 94 deletions.
12 changes: 9 additions & 3 deletions packages/plasma-b2c/api/plasma-b2c.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,11 @@ import { PopoverPlacement } from '@salutejs/plasma-hope';
import { PopoverProps } from '@salutejs/plasma-hope';
import { Popup } from '@salutejs/plasma-hope';
import { PopupBase } from '@salutejs/plasma-hope';
import { PopupBaseContext } from '@salutejs/plasma-hope';
import { PopupBasePlacement } from '@salutejs/plasma-hope';
import { PopupBaseProps } from '@salutejs/plasma-hope';
import { PopupBaseProvider } from '@salutejs/plasma-hope';
import { PopupContextType } from '@salutejs/plasma-hope';
import { PopupInfo } from '@salutejs/plasma-hope';
import { PopupProps } from '@salutejs/plasma-hope';
import { PreviewGallery } from '@salutejs/plasma-hope';
import { PreviewGalleryItemProps } from '@salutejs/plasma-hope';
Expand Down Expand Up @@ -264,6 +265,7 @@ import { useDebouncedFunction } from '@salutejs/plasma-core';
import { useFocusTrap } from '@salutejs/plasma-hope';
import { useForkRef } from '@salutejs/plasma-core';
import { useIsomorphicLayoutEffect } from '@salutejs/plasma-core';
import { usePopupBaseContext } from '@salutejs/plasma-hope';
import { useToast } from '@salutejs/plasma-hope';
import { ValidationResult } from '@salutejs/plasma-hope';
import { View } from '@salutejs/plasma-core';
Expand Down Expand Up @@ -651,14 +653,16 @@ export { Popup }

export { PopupBase }

export { PopupBaseContext }

export { PopupBasePlacement }

export { PopupBaseProps }

export { PopupBaseProvider }

export { PopupContextType }

export { PopupInfo }

export { PopupProps }

export { PreviewGallery }
Expand Down Expand Up @@ -873,6 +877,8 @@ export { useForkRef }

export { useIsomorphicLayoutEffect }

export { usePopupBaseContext }

export { useToast }

export { ValidationResult }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const StyledWrapper = styled.div`
height: 1200px;
`;

// TODO: новый отдельный оверлей #778
const ModalOverlayVariables = createGlobalStyle`
body {
--plasma-modal-blur-overlay-color: ${darkOverlayBlur};
Expand Down
3 changes: 2 additions & 1 deletion packages/plasma-b2c/src/components/PopupBase/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { PopupBaseProvider, PopupBaseContext } from '@salutejs/plasma-hope';
export { PopupBaseProvider, usePopupBaseContext } from '@salutejs/plasma-hope';
export type { PopupInfo, PopupContextType } from '@salutejs/plasma-hope';

export { PopupBase } from '@salutejs/plasma-hope';
export type { PopupBaseProps, PopupBasePlacement } from '@salutejs/plasma-hope';
28 changes: 22 additions & 6 deletions packages/plasma-core/api/plasma-core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -859,11 +859,6 @@ export const Popup: React_2.NamedExoticComponent<PopupProps & React_2.RefAttribu
// @public
export const PopupBase: React_2.ForwardRefExoticComponent<PopupBaseProps & React_2.RefAttributes<HTMLDivElement>>;

// Warning: (ae-forgotten-export) The symbol "PopupBaseController" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export const PopupBaseContext: React_2.Context<PopupBaseController>;

// Warning: (ae-forgotten-export) The symbol "BasicPopupBasePlacement" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "MixedPopupBasePlacement" needs to be exported by the entry point index.d.ts
//
Expand All @@ -877,9 +872,9 @@ export interface PopupBaseProps extends React_2.HTMLAttributes<HTMLDivElement> {
isOpen: boolean;
// (undocumented)
offset?: [number | string, number | string];
overlay?: React_2.ReactNode;
// (undocumented)
placement?: PopupBasePlacement;
// Warning: (ae-forgotten-export) The symbol "PopupInfo" needs to be exported by the entry point index.d.ts
popupInfo?: PopupInfo;
zIndex?: string;
}
Expand All @@ -889,6 +884,24 @@ export const PopupBaseProvider: React_2.FC<{
children: ReactNode;
}>;

// @public (undocumented)
export interface PopupContextType {
// (undocumented)
items: PopupInfo[];
// (undocumented)
register: (info: PopupInfo) => void;
// (undocumented)
unregister: (id: string) => void;
}

// @public (undocumented)
export interface PopupInfo {
// (undocumented)
id: string;
// (undocumented)
info?: Object;
}

// @public (undocumented)
export interface PopupProps extends HTMLAttributes<HTMLDivElement> {
children?: ReactNode;
Expand Down Expand Up @@ -1334,6 +1347,9 @@ export const usePaginationDots: ({ items, index, visibleItems }: SmartPagination
activeId: string | number;
};

// @public (undocumented)
export const usePopupBaseContext: () => PopupContextType;

// @public
export const useResizeObserver: <T extends HTMLElement>(ref: MutableRefObject<T | null>, callback: (element: T) => void) => void;

Expand Down
53 changes: 28 additions & 25 deletions packages/plasma-core/src/components/ModalBase/ModalBase.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { useCallback, useContext, FC, useEffect } from 'react';
import React, { useCallback, FC, useEffect } from 'react';
import styled, { createGlobalStyle, css } from 'styled-components';

import { useFocusTrap, useUniqId } from '../../hooks';
import { PopupBaseContext, PopupInfo } from '../PopupBase/PopupBaseContext';
import { usePopupBaseContext } from '../PopupBase/PopupBaseContext';
import { DEFAULT_Z_INDEX, PopupBase, PopupBaseProps } from '../PopupBase/PopupBase';

import { ModalInfo, getIdLastModal } from './ModalBaseContext';

export interface ModalBaseProps extends Omit<PopupBaseProps, 'frame'> {
/**
* Нужно ли применять blur для подложки.
Expand Down Expand Up @@ -41,6 +43,7 @@ export interface ModalBaseProps extends Omit<PopupBaseProps, 'frame'> {
onClose?: () => void;
}

// TODO: новый отдельный оверлей #778
const StyledOverlay = styled.div<{ transparent?: boolean; $withBlur?: boolean; clickable?: boolean; zIndex?: string }>`
position: absolute;
Expand Down Expand Up @@ -97,11 +100,11 @@ export const ModalBase: FC<ModalBaseProps> = ({
}) => {
const uniqId = useUniqId();
const innerId = id || uniqId;
const controller = useContext(PopupBaseContext);
const popupController = usePopupBaseContext();

const trapRef = useFocusTrap(true, initialFocusRef, focusAfterRef);

const onOverlayClickCallback = useCallback(
const onOverlayKeyDown = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
if (!closeOnOverlayClick) {
return;
Expand All @@ -112,62 +115,62 @@ export const ModalBase: FC<ModalBaseProps> = ({
return;
}

onClose?.();
if (onClose) {
onClose();
}
},
[closeOnOverlayClick, onOverlayClick, onClose],
);

// Вызов обработчика текущего окна
const onOverlayKeyDown = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
controller.callCurrentModalClose(event);
},
[controller.items],
);

// При ESC закрывает текущее окно, если это возможно
const onKeyDown = useCallback(
(event: KeyboardEvent) => {
if (closeOnEsc && event.keyCode === ESCAPE_KEYCODE && controller.getIdLastModal() === innerId) {
if (closeOnEsc && event.keyCode === ESCAPE_KEYCODE && getIdLastModal(popupController.items) === innerId) {
if (onEscKeyDown) {
onEscKeyDown(event);
return;
}

onClose?.();
if (onClose) {
onClose();
}
}
},
[onClose, onEscKeyDown, controller.items, closeOnEsc],
[onClose, onEscKeyDown, popupController, closeOnEsc],
);

useEffect(() => {
window.addEventListener('keydown', onKeyDown);
return () => {
window.removeEventListener('keydown', onKeyDown);
};
}, [onClose, onEscKeyDown, controller.items, closeOnEsc]);
}, [onClose, onEscKeyDown, popupController, closeOnEsc]);

const modalInfo: PopupInfo = {
const modalInfo: ModalInfo = {
id: innerId,
isModal: true,
onOverlayClick: onOverlayClickCallback,
info: {
isModal: true,
},
};

if (!isOpen) {
return null;
}

return (
<>
<NoScroll />
<StyledOverlay clickable={closeOnOverlayClick} onClick={onOverlayKeyDown} $withBlur={withBlur} />
<PopupBase
id={innerId}
frame="document"
isOpen={isOpen}
placement={placement}
ref={trapRef}
popupInfo={modalInfo}
overlay={
<StyledOverlay
transparent={getIdLastModal(popupController.items) !== innerId}
clickable={closeOnOverlayClick}
onClick={onOverlayKeyDown}
$withBlur={withBlur}
/>
}
{...rest}
>
{children}
Expand Down
21 changes: 21 additions & 0 deletions packages/plasma-core/src/components/ModalBase/ModalBaseContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { PopupInfo } from '../PopupBase/PopupBaseContext';

export interface ModalInfo extends PopupInfo {
id: string;
info?: {
isModal?: true;
};
}

/**
* Взаимодействие с модальными оконами.
*/
const getLastModal = (items: ModalInfo[]) => {
const modals = items.filter((item: ModalInfo) => item?.info?.isModal);
const lastModal = modals && (modals[modals.length - 1] as ModalInfo);

Check warning

Code scanning / Semgrep

Semgrep Finding: gitlab.eslint.detect-object-injection Warning

Bracket object notation with user input is present, this might allow an attacker to access all properties of the object and even it's prototype, leading to possible code execution.
return lastModal;
};

export const getIdLastModal = (items: ModalInfo[]) => {
return getLastModal(items)?.id;
};
52 changes: 35 additions & 17 deletions packages/plasma-core/src/components/PopupBase/PopupBase.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useEffect, useRef, useState, useContext } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import styled, { css } from 'styled-components';

import { useUniqId } from '../../hooks';

import { PopupBaseContext, POPOVER_PORTAL_ID, PopupInfo } from './PopupBaseContext';
import { POPOVER_PORTAL_ID, PopupInfo, usePopupBaseContext } from './PopupBaseContext';

type BasicPopupBasePlacement = 'center' | 'top' | 'bottom' | 'right' | 'left';
type MixedPopupBasePlacement = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
Expand Down Expand Up @@ -33,6 +33,10 @@ export interface PopupBaseProps extends React.HTMLAttributes<HTMLDivElement> {
* Содержимое PopupBase.
*/
children?: React.ReactNode;
/**
* Соседний элемент для окна в портале.
*/
overlay?: React.ReactNode;
/**
* Значение z-index для PopupBase.
*/
Expand Down Expand Up @@ -138,13 +142,19 @@ const PopupBaseRoot = styled.div<HidingProps & PopupBaseRootProps>`
*/

export const PopupBase = React.forwardRef<HTMLDivElement, PopupBaseProps>(
({ id, isOpen, placement, offset, frame = 'document', children, role, zIndex, popupInfo, ...rest }, ref) => {
(
{ id, isOpen, placement, offset, frame = 'document', children, overlay, role, zIndex, popupInfo, ...rest },
ref,
) => {
// Внутренее состояние, необходимое для правильного отображения вложенных окон, а также для анимации
const [isClosed, setClosed] = useState(!isOpen);

const uniqId = useUniqId();
const innerId = id || uniqId;

const portalRef = useRef<HTMLElement | null>(null);

const controller = useContext(PopupBaseContext);
const popupController = usePopupBaseContext();

const [, forceRender] = useState(false);

Expand All @@ -168,33 +178,41 @@ export const PopupBase = React.forwardRef<HTMLDivElement, PopupBaseProps>(
* отобразился после записи DOM элемента в portalRef.current
*/
forceRender(true);

return () => {
controller.unregister(innerId);
};
}, [controller, innerId, zIndex]);
}, []);

useEffect(() => {
// сначала добавление/удаление из контекста
if (isOpen) {
controller.register({ id: innerId, ...popupInfo });
popupController.register({ id: innerId, ...popupInfo });
} else {
controller.unregister(innerId);
popupController.unregister(innerId);
}
// затем отображение
setClosed(!isOpen);
}, [isOpen]);

if (!isOpen) {
if (isClosed) {
return null;
}

return (
<>
{portalRef.current &&
ReactDOM.createPortal(
<PopupBaseRoot ref={ref} placement={placement} frame={frame} offset={offset} zIndex={zIndex}>
<PopupBaseView {...rest} role={role}>
{children}
</PopupBaseView>
</PopupBaseRoot>,
<>
{overlay}
<PopupBaseRoot
ref={ref}
placement={placement}
frame={frame}
offset={offset}
zIndex={zIndex}
>
<PopupBaseView {...rest} role={role}>
{children}
</PopupBaseView>
</PopupBaseRoot>
</>,
portalRef.current,
)}
</>
Expand Down
Loading

0 comments on commit e052d20

Please sign in to comment.