From e052d202b8943a8b177d9ab3a18ff500b718aeca Mon Sep 17 00:00:00 2001 From: Ivan Kudryavtsev Date: Thu, 28 Sep 2023 12:46:18 +0300 Subject: [PATCH] fix(plasma-core): refactor in PopupBase/ModalBase --- packages/plasma-b2c/api/plasma-b2c.api.md | 12 +++- .../ModalBase/ModalBase.stories.tsx | 1 + .../src/components/PopupBase/index.ts | 3 +- packages/plasma-core/api/plasma-core.api.md | 28 ++++++-- .../src/components/ModalBase/ModalBase.tsx | 53 +++++++------- .../components/ModalBase/ModalBaseContext.tsx | 21 ++++++ .../src/components/PopupBase/PopupBase.tsx | 52 +++++++++----- .../components/PopupBase/PopupBaseContext.tsx | 72 ++++++++++--------- .../src/components/PopupBase/index.ts | 3 +- packages/plasma-hope/api/plasma-hope.api.md | 12 +++- .../src/components/PopupBase/index.ts | 3 +- packages/plasma-web/api/plasma-web.api.md | 12 +++- .../ModalBase/ModalBase.stories.tsx | 1 + .../src/components/PopupBase/index.ts | 3 +- 14 files changed, 182 insertions(+), 94 deletions(-) create mode 100644 packages/plasma-core/src/components/ModalBase/ModalBaseContext.tsx diff --git a/packages/plasma-b2c/api/plasma-b2c.api.md b/packages/plasma-b2c/api/plasma-b2c.api.md index a8301a6417..a47fbc57f1 100644 --- a/packages/plasma-b2c/api/plasma-b2c.api.md +++ b/packages/plasma-b2c/api/plasma-b2c.api.md @@ -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'; @@ -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'; @@ -651,14 +653,16 @@ export { Popup } export { PopupBase } -export { PopupBaseContext } - export { PopupBasePlacement } export { PopupBaseProps } export { PopupBaseProvider } +export { PopupContextType } + +export { PopupInfo } + export { PopupProps } export { PreviewGallery } @@ -873,6 +877,8 @@ export { useForkRef } export { useIsomorphicLayoutEffect } +export { usePopupBaseContext } + export { useToast } export { ValidationResult } diff --git a/packages/plasma-b2c/src/components/ModalBase/ModalBase.stories.tsx b/packages/plasma-b2c/src/components/ModalBase/ModalBase.stories.tsx index b45c90b3d9..5fc3f27646 100644 --- a/packages/plasma-b2c/src/components/ModalBase/ModalBase.stories.tsx +++ b/packages/plasma-b2c/src/components/ModalBase/ModalBase.stories.tsx @@ -51,6 +51,7 @@ const StyledWrapper = styled.div` height: 1200px; `; +// TODO: новый отдельный оверлей #778 const ModalOverlayVariables = createGlobalStyle` body { --plasma-modal-blur-overlay-color: ${darkOverlayBlur}; diff --git a/packages/plasma-b2c/src/components/PopupBase/index.ts b/packages/plasma-b2c/src/components/PopupBase/index.ts index 8a274eead0..16f402bad9 100644 --- a/packages/plasma-b2c/src/components/PopupBase/index.ts +++ b/packages/plasma-b2c/src/components/PopupBase/index.ts @@ -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'; diff --git a/packages/plasma-core/api/plasma-core.api.md b/packages/plasma-core/api/plasma-core.api.md index b514cf302e..4cc9e68939 100644 --- a/packages/plasma-core/api/plasma-core.api.md +++ b/packages/plasma-core/api/plasma-core.api.md @@ -859,11 +859,6 @@ export const Popup: React_2.NamedExoticComponent>; -// 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; - // 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 // @@ -877,9 +872,9 @@ export interface PopupBaseProps extends React_2.HTMLAttributes { 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; } @@ -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 { children?: ReactNode; @@ -1334,6 +1347,9 @@ export const usePaginationDots: ({ items, index, visibleItems }: SmartPagination activeId: string | number; }; +// @public (undocumented) +export const usePopupBaseContext: () => PopupContextType; + // @public export const useResizeObserver: (ref: MutableRefObject, callback: (element: T) => void) => void; diff --git a/packages/plasma-core/src/components/ModalBase/ModalBase.tsx b/packages/plasma-core/src/components/ModalBase/ModalBase.tsx index 1fa14101d3..4d90e2c746 100644 --- a/packages/plasma-core/src/components/ModalBase/ModalBase.tsx +++ b/packages/plasma-core/src/components/ModalBase/ModalBase.tsx @@ -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 { /** * Нужно ли применять blur для подложки. @@ -41,6 +43,7 @@ export interface ModalBaseProps extends Omit { onClose?: () => void; } +// TODO: новый отдельный оверлей #778 const StyledOverlay = styled.div<{ transparent?: boolean; $withBlur?: boolean; clickable?: boolean; zIndex?: string }>` position: absolute; @@ -97,11 +100,11 @@ export const ModalBase: FC = ({ }) => { 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) => { if (!closeOnOverlayClick) { return; @@ -112,32 +115,28 @@ export const ModalBase: FC = ({ return; } - onClose?.(); + if (onClose) { + onClose(); + } }, [closeOnOverlayClick, onOverlayClick, onClose], ); - // Вызов обработчика текущего окна - const onOverlayKeyDown = useCallback( - (event: React.MouseEvent) => { - 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(() => { @@ -145,22 +144,18 @@ export const ModalBase: FC = ({ 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 ( <> - = ({ placement={placement} ref={trapRef} popupInfo={modalInfo} + overlay={ + + } {...rest} > {children} diff --git a/packages/plasma-core/src/components/ModalBase/ModalBaseContext.tsx b/packages/plasma-core/src/components/ModalBase/ModalBaseContext.tsx new file mode 100644 index 0000000000..b9ce0d2312 --- /dev/null +++ b/packages/plasma-core/src/components/ModalBase/ModalBaseContext.tsx @@ -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); + return lastModal; +}; + +export const getIdLastModal = (items: ModalInfo[]) => { + return getLastModal(items)?.id; +}; diff --git a/packages/plasma-core/src/components/PopupBase/PopupBase.tsx b/packages/plasma-core/src/components/PopupBase/PopupBase.tsx index fea575f1c2..1f5b371ca7 100644 --- a/packages/plasma-core/src/components/PopupBase/PopupBase.tsx +++ b/packages/plasma-core/src/components/PopupBase/PopupBase.tsx @@ -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'; @@ -33,6 +33,10 @@ export interface PopupBaseProps extends React.HTMLAttributes { * Содержимое PopupBase. */ children?: React.ReactNode; + /** + * Соседний элемент для окна в портале. + */ + overlay?: React.ReactNode; /** * Значение z-index для PopupBase. */ @@ -138,13 +142,19 @@ const PopupBaseRoot = styled.div` */ export const PopupBase = React.forwardRef( - ({ 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(null); - const controller = useContext(PopupBaseContext); + const popupController = usePopupBaseContext(); const [, forceRender] = useState(false); @@ -168,21 +178,20 @@ export const PopupBase = React.forwardRef( * отобразился после записи 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; } @@ -190,11 +199,20 @@ export const PopupBase = React.forwardRef( <> {portalRef.current && ReactDOM.createPortal( - - - {children} - - , + <> + {overlay} + + + {children} + + + , portalRef.current, )} diff --git a/packages/plasma-core/src/components/PopupBase/PopupBaseContext.tsx b/packages/plasma-core/src/components/PopupBase/PopupBaseContext.tsx index e0bedb081f..d47450e847 100644 --- a/packages/plasma-core/src/components/PopupBase/PopupBaseContext.tsx +++ b/packages/plasma-core/src/components/PopupBase/PopupBaseContext.tsx @@ -2,49 +2,55 @@ import React, { ReactNode, useEffect } from 'react'; export interface PopupInfo { id: string; - isModal?: true; - onOverlayClick?: (event: React.MouseEvent) => void; + info?: Object; } -/** - * Хранилище модальных окон. - */ -class PopupBaseController { - public items: PopupInfo[] = []; +export const POPOVER_PORTAL_ID = 'plasma-popup-root'; - public register(info: PopupInfo) { - return this.items.push(info); - } +const items: PopupInfo[] = []; - public unregister(id: string) { - const index = this.items.findIndex((item: PopupInfo) => id === item.id); - if (index === -1) { - return; - } - this.items.splice(index, 1); - } +export interface PopupContextType { + items: PopupInfo[]; + register: (info: PopupInfo) => void; + unregister: (id: string) => void; +} - getLastModal() { - const modals = this.items.filter((item: PopupInfo) => item.isModal); - return modals && modals[modals.length - 1]; - } +const PopupBaseContext = React.createContext({ + items, + register(_info: PopupInfo): void { + throw new Error('Function not implemented.'); + }, + unregister(_id: string): void { + throw new Error('Function not implemented.'); + }, +}); - public getIdLastModal() { - return this.getLastModal()?.id; - } +export const usePopupBaseContext = () => React.useContext(PopupBaseContext); - public callCurrentModalClose(event: React.MouseEvent) { - this.getLastModal()?.onOverlayClick?.(event); - } -} +export const PopupBaseProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [items, setItems] = React.useState([]); -const controller = new PopupBaseController(); + const register = (info: PopupInfo) => { + const updatedItems = [...items]; + updatedItems.push(info); + setItems(updatedItems); + }; -export const POPOVER_PORTAL_ID = 'plasma-popup-root'; + const unregister = (id: string) => { + const index = items.findIndex((item: PopupInfo) => id === item.id); + if (index === -1) { + return; + } + items.splice(index, 1); + setItems([...items]); + }; -export const PopupBaseContext = React.createContext(controller); + const context = { + items, + register, + unregister, + }; -export const PopupBaseProvider: React.FC<{ children: ReactNode }> = ({ children }) => { useEffect(() => { return () => { const portal = document.createElement('div'); @@ -54,5 +60,5 @@ export const PopupBaseProvider: React.FC<{ children: ReactNode }> = ({ children }; }, []); - return {children}; + return {children}; }; diff --git a/packages/plasma-core/src/components/PopupBase/index.ts b/packages/plasma-core/src/components/PopupBase/index.ts index a8736de25a..326db4367d 100644 --- a/packages/plasma-core/src/components/PopupBase/index.ts +++ b/packages/plasma-core/src/components/PopupBase/index.ts @@ -1,4 +1,5 @@ -export { PopupBaseProvider, PopupBaseContext } from './PopupBaseContext'; +export { PopupBaseProvider, usePopupBaseContext } from './PopupBaseContext'; +export type { PopupInfo, PopupContextType } from './PopupBaseContext'; export { PopupBase } from './PopupBase'; export type { PopupBaseProps, PopupBasePlacement } from './PopupBase'; diff --git a/packages/plasma-hope/api/plasma-hope.api.md b/packages/plasma-hope/api/plasma-hope.api.md index bb3da6f144..bea958f125 100644 --- a/packages/plasma-hope/api/plasma-hope.api.md +++ b/packages/plasma-hope/api/plasma-hope.api.md @@ -116,10 +116,11 @@ import { PopoverPlacement } from '@salutejs/plasma-core'; import { PopoverProps } from '@salutejs/plasma-core'; import { Popup } from '@salutejs/plasma-core'; import { PopupBase } from '@salutejs/plasma-core'; -import { PopupBaseContext } from '@salutejs/plasma-core'; import { PopupBasePlacement } from '@salutejs/plasma-core'; import { PopupBaseProps } from '@salutejs/plasma-core'; import { PopupBaseProvider } from '@salutejs/plasma-core'; +import { PopupContextType } from '@salutejs/plasma-core'; +import { PopupInfo } from '@salutejs/plasma-core'; import { PopupProps } from '@salutejs/plasma-core'; import type { PriceProps as PriceProps_2 } from '@salutejs/plasma-core'; import { PropsWithChildren } from 'react'; @@ -175,6 +176,7 @@ import { useDebouncedFunction } from '@salutejs/plasma-core'; import { useFocusTrap } from '@salutejs/plasma-core'; import { useForkRef } from '@salutejs/plasma-core'; import { useIsomorphicLayoutEffect } from '@salutejs/plasma-core'; +import { usePopupBaseContext } from '@salutejs/plasma-core'; import { useResizeObserver } from '@salutejs/plasma-core'; import { useToast } from '@salutejs/plasma-core'; import { View } from '@salutejs/plasma-core'; @@ -1004,14 +1006,16 @@ export { Popup } export { PopupBase } -export { PopupBaseContext } - export { PopupBasePlacement } export { PopupBaseProps } export { PopupBaseProvider } +export { PopupContextType } + +export { PopupInfo } + export { PopupProps } // @public @@ -1351,6 +1355,8 @@ export { useForkRef } export { useIsomorphicLayoutEffect } +export { usePopupBaseContext } + export { useResizeObserver } export { useToast } diff --git a/packages/plasma-hope/src/components/PopupBase/index.ts b/packages/plasma-hope/src/components/PopupBase/index.ts index 9636e9486c..1cb192a38a 100644 --- a/packages/plasma-hope/src/components/PopupBase/index.ts +++ b/packages/plasma-hope/src/components/PopupBase/index.ts @@ -1,4 +1,5 @@ -export { PopupBaseProvider, PopupBaseContext } from '@salutejs/plasma-core'; +export { PopupBaseProvider, usePopupBaseContext } from '@salutejs/plasma-core'; +export type { PopupInfo, PopupContextType } from '@salutejs/plasma-core'; export { PopupBase } from '@salutejs/plasma-core'; export type { PopupBaseProps, PopupBasePlacement } from '@salutejs/plasma-core'; diff --git a/packages/plasma-web/api/plasma-web.api.md b/packages/plasma-web/api/plasma-web.api.md index 68ed111577..bb0f147b66 100644 --- a/packages/plasma-web/api/plasma-web.api.md +++ b/packages/plasma-web/api/plasma-web.api.md @@ -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'; @@ -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'; @@ -651,14 +653,16 @@ export { Popup } export { PopupBase } -export { PopupBaseContext } - export { PopupBasePlacement } export { PopupBaseProps } export { PopupBaseProvider } +export { PopupContextType } + +export { PopupInfo } + export { PopupProps } export { PreviewGallery } @@ -873,6 +877,8 @@ export { useForkRef } export { useIsomorphicLayoutEffect } +export { usePopupBaseContext } + export { useToast } export { ValidationResult } diff --git a/packages/plasma-web/src/components/ModalBase/ModalBase.stories.tsx b/packages/plasma-web/src/components/ModalBase/ModalBase.stories.tsx index 41454833a4..09757540df 100644 --- a/packages/plasma-web/src/components/ModalBase/ModalBase.stories.tsx +++ b/packages/plasma-web/src/components/ModalBase/ModalBase.stories.tsx @@ -51,6 +51,7 @@ const StyledWrapper = styled.div` height: 1200px; `; +// TODO: новый отдельный оверлей #778 const ModalOverlayVariables = createGlobalStyle` body { --plasma-modal-blur-overlay-color: ${darkOverlayBlur}; diff --git a/packages/plasma-web/src/components/PopupBase/index.ts b/packages/plasma-web/src/components/PopupBase/index.ts index 8a274eead0..16f402bad9 100644 --- a/packages/plasma-web/src/components/PopupBase/index.ts +++ b/packages/plasma-web/src/components/PopupBase/index.ts @@ -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';