From fe9656efabe6ca5b130fc1949efab62b3ee6802c Mon Sep 17 00:00:00 2001 From: Ivan Kudryavtsev Date: Tue, 26 Sep 2023 13:19:52 +0300 Subject: [PATCH 1/2] feat(plasma-core, plasma-web, plasma-b2c): ModalBase component --- packages/plasma-b2c/api/plasma-b2c.api.md | 12 ++ .../ModalBase/ModalBase.stories.tsx | 139 ++++++++++++++ .../src/components/ModalBase/index.ts | 2 + .../PopupBase/PopupBase.stories.tsx | 42 +++-- .../src/components/PopupBase/index.ts | 2 + packages/plasma-b2c/src/index.ts | 1 + packages/plasma-core/api/plasma-core.api.md | 29 ++- .../src/components/ModalBase/ModalBase.tsx | 177 ++++++++++++++++++ .../src/components/ModalBase/index.ts | 2 + .../src/components/PopupBase/PopupBase.tsx | 128 ++++++------- .../components/PopupBase/PopupBaseContext.tsx | 31 ++- .../src/components/PopupBase/index.ts | 2 + packages/plasma-core/src/index.ts | 1 + packages/plasma-hope/api/plasma-hope.api.md | 12 ++ .../src/components/ModalBase/index.ts | 2 + .../src/components/PopupBase/index.ts | 2 + packages/plasma-hope/src/index.ts | 1 + packages/plasma-web/api/plasma-web.api.md | 12 ++ .../ModalBase/ModalBase.stories.tsx | 139 ++++++++++++++ .../src/components/ModalBase/index.ts | 2 + .../PopupBase/PopupBase.stories.tsx | 42 +++-- .../src/components/PopupBase/index.ts | 2 + packages/plasma-web/src/index.ts | 1 + 23 files changed, 675 insertions(+), 108 deletions(-) create mode 100644 packages/plasma-b2c/src/components/ModalBase/ModalBase.stories.tsx create mode 100644 packages/plasma-b2c/src/components/ModalBase/index.ts create mode 100644 packages/plasma-core/src/components/ModalBase/ModalBase.tsx create mode 100644 packages/plasma-core/src/components/ModalBase/index.ts create mode 100644 packages/plasma-hope/src/components/ModalBase/index.ts create mode 100644 packages/plasma-web/src/components/ModalBase/ModalBase.stories.tsx create mode 100644 packages/plasma-web/src/components/ModalBase/index.ts diff --git a/packages/plasma-b2c/api/plasma-b2c.api.md b/packages/plasma-b2c/api/plasma-b2c.api.md index 30a570ab87..a8301a6417 100644 --- a/packages/plasma-b2c/api/plasma-b2c.api.md +++ b/packages/plasma-b2c/api/plasma-b2c.api.md @@ -146,6 +146,8 @@ import { MaxLinesProps } from '@salutejs/plasma-core'; import { mediaQuery } from '@salutejs/plasma-hope'; import { MediaQueryFunction } from '@salutejs/plasma-hope'; import { Modal } from '@salutejs/plasma-hope'; +import { ModalBase } from '@salutejs/plasma-hope'; +import { ModalBaseProps } from '@salutejs/plasma-hope'; import { ModalProps } from '@salutejs/plasma-hope'; import { ModalsProvider } from '@salutejs/plasma-hope'; import { ModalView } from '@salutejs/plasma-hope'; @@ -172,8 +174,10 @@ 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 { PopupProps } from '@salutejs/plasma-hope'; import { PreviewGallery } from '@salutejs/plasma-hope'; import { PreviewGalleryItemProps } from '@salutejs/plasma-hope'; @@ -591,6 +595,10 @@ export { MediaQueryFunction } export { Modal } +export { ModalBase } + +export { ModalBaseProps } + export { ModalProps } export { ModalsProvider } @@ -643,10 +651,14 @@ export { Popup } export { PopupBase } +export { PopupBaseContext } + export { PopupBasePlacement } export { PopupBaseProps } +export { PopupBaseProvider } + export { PopupProps } export { PreviewGallery } diff --git a/packages/plasma-b2c/src/components/ModalBase/ModalBase.stories.tsx b/packages/plasma-b2c/src/components/ModalBase/ModalBase.stories.tsx new file mode 100644 index 0000000000..b45c90b3d9 --- /dev/null +++ b/packages/plasma-b2c/src/components/ModalBase/ModalBase.stories.tsx @@ -0,0 +1,139 @@ +import React from 'react'; +import styled, { createGlobalStyle } from 'styled-components'; +import { Story, Meta } from '@storybook/react'; +import { surfaceSolid02, darkOverlayBlur, overlaySoft } from '@salutejs/plasma-tokens-web'; +import { InSpacingDecorator } from '@salutejs/plasma-sb-utils'; + +import { SSRProvider } from '../SSRProvider'; +import { Button } from '../Button'; +import { PopupBaseProvider } from '../PopupBase'; + +import { ModalBase } from '.'; + +export default { + title: 'Controls/ModalBase', + decorators: [InSpacingDecorator], + argTypes: { + placement: { + options: [ + 'center', + 'top', + 'bottom', + 'right', + 'left', + 'top-right', + 'top-left', + 'bottom-right', + 'bottom-left', + ], + control: { + type: 'select', + }, + }, + }, +} as Meta; + +type ModalBaseStoryProps = { + placement: string; + offsetX: number; + offsetY: number; + closeOnEsc: boolean; + closeOnOverlayClick: boolean; + withBlur: boolean; +}; + +const StyledButton = styled(Button)` + margin-top: 1rem; + width: 15rem; +`; + +const StyledWrapper = styled.div` + height: 1200px; +`; + +const ModalOverlayVariables = createGlobalStyle` + body { + --plasma-modal-blur-overlay-color: ${darkOverlayBlur}; + --plasma-modal-overlay-color: ${overlaySoft}; + } +`; + +const Content = styled.div` + background: ${surfaceSolid02}; + padding: 1rem; +`; + +export const ModalBaseDemo: Story = ({ placement, offsetX, offsetY, ...rest }) => { + const [isOpenA, setIsOpenA] = React.useState(false); + const [isOpenB, setIsOpenB] = React.useState(false); + const [isOpenC, setIsOpenC] = React.useState(false); + + return ( + + + + +
+ setIsOpenA(true)} /> +
+ setIsOpenA(false)} + isOpen={isOpenA} + placement={placement} + offset={[offsetX, offsetY]} + {...rest} + > + + +
+ setIsOpenB(true)} /> +
+ setIsOpenB(false)} + isOpen={isOpenB} + placement="left" + offset={[offsetX, offsetY]} + {...rest} + > + + +
+ setIsOpenC(true)} /> +
+ setIsOpenC(false)} + isOpen={isOpenC} + placement="top" + offset={[offsetX, offsetY]} + {...rest} + > + + + <>Content + + +
+
+
+
+
+
+
+ ); +}; + +ModalBaseDemo.args = { + placement: 'center', + withBlur: false, + closeOnEsc: true, + closeOnOverlayClick: true, + offsetX: 0, + offsetY: 0, +}; diff --git a/packages/plasma-b2c/src/components/ModalBase/index.ts b/packages/plasma-b2c/src/components/ModalBase/index.ts new file mode 100644 index 0000000000..3d4e2af155 --- /dev/null +++ b/packages/plasma-b2c/src/components/ModalBase/index.ts @@ -0,0 +1,2 @@ +export { ModalBase } from '@salutejs/plasma-hope'; +export type { ModalBaseProps } from '@salutejs/plasma-hope'; diff --git a/packages/plasma-b2c/src/components/PopupBase/PopupBase.stories.tsx b/packages/plasma-b2c/src/components/PopupBase/PopupBase.stories.tsx index a2f3220db9..a83ab16898 100644 --- a/packages/plasma-b2c/src/components/PopupBase/PopupBase.stories.tsx +++ b/packages/plasma-b2c/src/components/PopupBase/PopupBase.stories.tsx @@ -7,7 +7,7 @@ import { surfaceSolid03, surfaceSolid02 } from '@salutejs/plasma-tokens-web'; import { SSRProvider } from '../SSRProvider'; import { Button } from '../Button'; -import { PopupBase } from '.'; +import { PopupBase, PopupBaseProvider } from '.'; export default { title: 'Controls/PopupBase', @@ -73,25 +73,27 @@ export const PopupBaseDemo: Story = ({ placement, offsetX, return ( -
- setIsOpenA(true)} /> - setIsOpenB(true)} /> -
- - - - <>Content - - - - <>Frame - - - - - <>Content - - + +
+ setIsOpenA(true)} /> + setIsOpenB(true)} /> +
+ + + + <>Content + + + + <>Frame + + + + + <>Content + + +
); diff --git a/packages/plasma-b2c/src/components/PopupBase/index.ts b/packages/plasma-b2c/src/components/PopupBase/index.ts index 312f45f3bf..8a274eead0 100644 --- a/packages/plasma-b2c/src/components/PopupBase/index.ts +++ b/packages/plasma-b2c/src/components/PopupBase/index.ts @@ -1,2 +1,4 @@ +export { PopupBaseProvider, PopupBaseContext } from '@salutejs/plasma-hope'; + export { PopupBase } from '@salutejs/plasma-hope'; export type { PopupBaseProps, PopupBasePlacement } from '@salutejs/plasma-hope'; diff --git a/packages/plasma-b2c/src/index.ts b/packages/plasma-b2c/src/index.ts index 648424960b..42433d06fc 100644 --- a/packages/plasma-b2c/src/index.ts +++ b/packages/plasma-b2c/src/index.ts @@ -14,6 +14,7 @@ export * from './components/Image'; export * from './components/Link'; export * from './components/List'; export * from './components/Modal'; +export * from './components/ModalBase'; export * from './components/Notification'; export * from './components/PaginationDots'; export * from './components/Popup'; diff --git a/packages/plasma-core/api/plasma-core.api.md b/packages/plasma-core/api/plasma-core.api.md index 031e167033..b514cf302e 100644 --- a/packages/plasma-core/api/plasma-core.api.md +++ b/packages/plasma-core/api/plasma-core.api.md @@ -752,6 +752,21 @@ export interface MaxLinesProps { maxLines?: number; } +// @public +export const ModalBase: FC; + +// @public (undocumented) +export interface ModalBaseProps extends Omit { + closeOnEsc?: boolean; + closeOnOverlayClick?: boolean; + focusAfterRef?: React_2.RefObject; + initialFocusRef?: React_2.RefObject; + onClose?: () => void; + onEscKeyDown?: (event: KeyboardEvent) => void; + onOverlayClick?: (event: React_2.MouseEvent) => void; + withBlur?: boolean; +} + // @public (undocumented) export const monthLongName: (val: number) => string; @@ -842,7 +857,12 @@ export interface PopoverProps extends HTMLAttributes { export const Popup: React_2.NamedExoticComponent>; // @public -export const PopupBase: FC; +export const PopupBase: React_2.ForwardRefExoticComponent>; + +// 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 @@ -859,9 +879,16 @@ export interface PopupBaseProps extends React_2.HTMLAttributes { offset?: [number | string, number | string]; // (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; } +// @public (undocumented) +export const PopupBaseProvider: React_2.FC<{ + children: ReactNode; +}>; + // @public (undocumented) export interface PopupProps extends HTMLAttributes { children?: ReactNode; diff --git a/packages/plasma-core/src/components/ModalBase/ModalBase.tsx b/packages/plasma-core/src/components/ModalBase/ModalBase.tsx new file mode 100644 index 0000000000..1fa14101d3 --- /dev/null +++ b/packages/plasma-core/src/components/ModalBase/ModalBase.tsx @@ -0,0 +1,177 @@ +import React, { useCallback, useContext, FC, useEffect } from 'react'; +import styled, { createGlobalStyle, css } from 'styled-components'; + +import { useFocusTrap, useUniqId } from '../../hooks'; +import { PopupBaseContext, PopupInfo } from '../PopupBase/PopupBaseContext'; +import { DEFAULT_Z_INDEX, PopupBase, PopupBaseProps } from '../PopupBase/PopupBase'; + +export interface ModalBaseProps extends Omit { + /** + * Нужно ли применять blur для подложки. + */ + withBlur?: boolean; + /** + * Закрывать модальное окно при нажатии на ESC(по умолчанию true). + */ + closeOnEsc?: boolean; + /** + * Закрывать модальное окно при нажатии вне области модального окна(по умолчанию true), + */ + closeOnOverlayClick?: boolean; + /** + * Обработчик клика при нажатии на ESC(если не передан, то при нажатии используется onClose). + */ + onEscKeyDown?: (event: KeyboardEvent) => void; + /** + * Обработчик клика при нажатии вне области модального окна(если не передан, то при нажатии используется onClose). + */ + onOverlayClick?: (event: React.MouseEvent) => void; + /** + * Первый элемент для фокуса внутри модального окна. + */ + initialFocusRef?: React.RefObject; + /** + * Элемент для фокуса после закрытия модального окна + * (по умолчанию фокус на последнем перед открытием активном элементе). + */ + focusAfterRef?: React.RefObject; + /** + * Общий обработчик клика по кнопке "закрыть". + */ + onClose?: () => void; +} + +const StyledOverlay = styled.div<{ transparent?: boolean; $withBlur?: boolean; clickable?: boolean; zIndex?: string }>` + position: absolute; + + width: 100%; + height: 100%; + + top: 0; + left: 0; + + ${({ zIndex }) => css` + z-index: ${zIndex || DEFAULT_Z_INDEX}; + `} + + ${({ $withBlur }) => { + return css` + --background-color: ${$withBlur + ? 'var(--plasma-modal-blur-overlay-color)' + : 'var(--plasma-modal-overlay-color)'}; + --backdrop-filter: ${$withBlur ? 'blur(1rem)' : 'none'}; + `; + }}; + + background-color: ${({ transparent }) => (transparent ? 'transparent' : 'var(--background-color)')}; + backdrop-filter: var(--backdrop-filter); + cursor: ${({ clickable }) => (clickable ? 'pointer' : 'default')}; +`; + +const NoScroll = createGlobalStyle` + body { + overflow-y: hidden; + } +`; + +const ESCAPE_KEYCODE = 27; + +/** + * ModalBase. + * Управляет показом/скрытием, подложкой и анимацией визуальной части модального окна. + */ +export const ModalBase: FC = ({ + id, + isOpen, + placement, + onClose, + onOverlayClick, + onEscKeyDown, + closeOnEsc = true, + closeOnOverlayClick = true, + withBlur, + initialFocusRef, + focusAfterRef, + children, + ...rest +}) => { + const uniqId = useUniqId(); + const innerId = id || uniqId; + const controller = useContext(PopupBaseContext); + + const trapRef = useFocusTrap(true, initialFocusRef, focusAfterRef); + + const onOverlayClickCallback = useCallback( + (event: React.MouseEvent) => { + if (!closeOnOverlayClick) { + return; + } + + if (onOverlayClick) { + onOverlayClick(event); + return; + } + + 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 (onEscKeyDown) { + onEscKeyDown(event); + return; + } + + onClose?.(); + } + }, + [onClose, onEscKeyDown, controller.items, closeOnEsc], + ); + + useEffect(() => { + window.addEventListener('keydown', onKeyDown); + return () => { + window.removeEventListener('keydown', onKeyDown); + }; + }, [onClose, onEscKeyDown, controller.items, closeOnEsc]); + + const modalInfo: PopupInfo = { + id: innerId, + isModal: true, + onOverlayClick: onOverlayClickCallback, + }; + + if (!isOpen) { + return null; + } + + return ( + <> + + + + {children} + + + ); +}; diff --git a/packages/plasma-core/src/components/ModalBase/index.ts b/packages/plasma-core/src/components/ModalBase/index.ts new file mode 100644 index 0000000000..f6c7678e33 --- /dev/null +++ b/packages/plasma-core/src/components/ModalBase/index.ts @@ -0,0 +1,2 @@ +export { ModalBase } from './ModalBase'; +export type { ModalBaseProps } from './ModalBase'; diff --git a/packages/plasma-core/src/components/PopupBase/PopupBase.tsx b/packages/plasma-core/src/components/PopupBase/PopupBase.tsx index 2da0686ae2..fea575f1c2 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, FC } from 'react'; +import React, { useEffect, useRef, useState, useContext } from 'react'; import ReactDOM from 'react-dom'; import styled, { css } from 'styled-components'; import { useUniqId } from '../../hooks'; -import { PopupBaseContext, POPOVER_PORTAL_ID } from './PopupBaseContext'; +import { PopupBaseContext, POPOVER_PORTAL_ID, PopupInfo } from './PopupBaseContext'; type BasicPopupBasePlacement = 'center' | 'top' | 'bottom' | 'right' | 'left'; type MixedPopupBasePlacement = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'; @@ -37,6 +37,10 @@ export interface PopupBaseProps extends React.HTMLAttributes { * Значение z-index для PopupBase. */ zIndex?: string; + /** + * Дополнительная информация для программного взаимодействия с окном через контекст. + */ + popupInfo?: PopupInfo; } interface HidingProps { @@ -132,70 +136,68 @@ const PopupBaseRoot = styled.div` * Базовый PopupBase. * Управляет показом/скрытием и анимацией(?) высплывающего окна. */ -export const PopupBase: FC = ({ - id, - isOpen, - placement, - offset, - frame = 'document', - children, - role, - zIndex, - ...rest -}) => { - const uniqId = useUniqId(); - const innerId = id || uniqId; - - const portalRef = useRef(null); - - const controller = useContext(PopupBaseContext); - - const [, forceRender] = useState(false); - - useEffect(() => { - let portal = document.getElementById(POPOVER_PORTAL_ID); - - if (frame !== 'document' && frame && frame.current) { - portal = frame.current; - } - if (!portal) { - portal = document.createElement('div'); - portal.setAttribute('id', POPOVER_PORTAL_ID); - document.body.appendChild(portal); - } +export const PopupBase = React.forwardRef( + ({ id, isOpen, placement, offset, frame = 'document', children, role, zIndex, popupInfo, ...rest }, ref) => { + const uniqId = useUniqId(); + const innerId = id || uniqId; - portalRef.current = portal; + const portalRef = useRef(null); - /** - * Изменение стейта нужно для того, чтобы PopupBase - * отобразился после записи DOM элемента в portalRef.current - */ - forceRender(true); + const controller = useContext(PopupBaseContext); - return () => { - controller.unregister(innerId); - }; - }, [controller, innerId, zIndex]); + const [, forceRender] = useState(false); - if (isOpen) { - controller.register(innerId); - } else { - controller.unregister(innerId); - return null; - } + useEffect(() => { + let portal = document.getElementById(POPOVER_PORTAL_ID); - return ( - <> - {portalRef.current && - ReactDOM.createPortal( - - - {children} - - , - portalRef.current, - )} - - ); -}; + if (frame !== 'document' && frame && frame.current) { + portal = frame.current; + } + + if (!portal) { + portal = document.createElement('div'); + portal.setAttribute('id', POPOVER_PORTAL_ID); + document.body.appendChild(portal); + } + + portalRef.current = portal; + + /** + * Изменение стейта нужно для того, чтобы PopupBase + * отобразился после записи DOM элемента в portalRef.current + */ + forceRender(true); + + return () => { + controller.unregister(innerId); + }; + }, [controller, innerId, zIndex]); + + useEffect(() => { + if (isOpen) { + controller.register({ id: innerId, ...popupInfo }); + } else { + controller.unregister(innerId); + } + }, [isOpen]); + + if (!isOpen) { + return null; + } + + return ( + <> + {portalRef.current && + ReactDOM.createPortal( + + + {children} + + , + portalRef.current, + )} + + ); + }, +); diff --git a/packages/plasma-core/src/components/PopupBase/PopupBaseContext.tsx b/packages/plasma-core/src/components/PopupBase/PopupBaseContext.tsx index 5b519ff416..e0bedb081f 100644 --- a/packages/plasma-core/src/components/PopupBase/PopupBaseContext.tsx +++ b/packages/plasma-core/src/components/PopupBase/PopupBaseContext.tsx @@ -1,17 +1,40 @@ import React, { ReactNode, useEffect } from 'react'; +export interface PopupInfo { + id: string; + isModal?: true; + onOverlayClick?: (event: React.MouseEvent) => void; +} + /** * Хранилище модальных окон. */ class PopupBaseController { - public items: string[] = []; + public items: PopupInfo[] = []; - public register(id: string) { - return this.items.push(id); + public register(info: PopupInfo) { + return this.items.push(info); } public unregister(id: string) { - this.items.splice(this.items.indexOf(id), 1); + const index = this.items.findIndex((item: PopupInfo) => id === item.id); + if (index === -1) { + return; + } + this.items.splice(index, 1); + } + + getLastModal() { + const modals = this.items.filter((item: PopupInfo) => item.isModal); + return modals && modals[modals.length - 1]; + } + + public getIdLastModal() { + return this.getLastModal()?.id; + } + + public callCurrentModalClose(event: React.MouseEvent) { + this.getLastModal()?.onOverlayClick?.(event); } } diff --git a/packages/plasma-core/src/components/PopupBase/index.ts b/packages/plasma-core/src/components/PopupBase/index.ts index e9ec8717f5..a8736de25a 100644 --- a/packages/plasma-core/src/components/PopupBase/index.ts +++ b/packages/plasma-core/src/components/PopupBase/index.ts @@ -1,2 +1,4 @@ +export { PopupBaseProvider, PopupBaseContext } from './PopupBaseContext'; + export { PopupBase } from './PopupBase'; export type { PopupBaseProps, PopupBasePlacement } from './PopupBase'; diff --git a/packages/plasma-core/src/index.ts b/packages/plasma-core/src/index.ts index 2774fbe9f9..48de7019c2 100644 --- a/packages/plasma-core/src/index.ts +++ b/packages/plasma-core/src/index.ts @@ -6,6 +6,7 @@ export * from './components/Fade'; export * from './components/Field'; export * from './components/Image'; export * from './components/Input'; +export * from './components/ModalBase'; export * from './components/PaginationDots'; export * from './components/Popup'; export * from './components/PopupBase'; diff --git a/packages/plasma-hope/api/plasma-hope.api.md b/packages/plasma-hope/api/plasma-hope.api.md index 38fe634d0d..bb3da6f144 100644 --- a/packages/plasma-hope/api/plasma-hope.api.md +++ b/packages/plasma-hope/api/plasma-hope.api.md @@ -94,6 +94,8 @@ import { KeyboardEvent as KeyboardEvent_2 } from 'react'; import { LineSkeleton } from '@salutejs/plasma-core'; import { LineSkeletonProps } from '@salutejs/plasma-core'; import { MaxLinesProps } from '@salutejs/plasma-core'; +import { ModalBase } from '@salutejs/plasma-core'; +import { ModalBaseProps } from '@salutejs/plasma-core'; import { monthLongName } from '@salutejs/plasma-core'; import { monthShortName } from '@salutejs/plasma-core'; import { MutableRefObject } from 'react'; @@ -114,8 +116,10 @@ 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 { PopupProps } from '@salutejs/plasma-core'; import type { PriceProps as PriceProps_2 } from '@salutejs/plasma-core'; import { PropsWithChildren } from 'react'; @@ -898,6 +902,10 @@ export type MediaQueryFunction = (content: FlattenSimpleInterpolation | string) // @public export const Modal: FC; +export { ModalBase } + +export { ModalBaseProps } + // @public (undocumented) export interface ModalProps extends ModalViewProps { closeOnEsc?: boolean; @@ -996,10 +1004,14 @@ export { Popup } export { PopupBase } +export { PopupBaseContext } + export { PopupBasePlacement } export { PopupBaseProps } +export { PopupBaseProvider } + export { PopupProps } // @public diff --git a/packages/plasma-hope/src/components/ModalBase/index.ts b/packages/plasma-hope/src/components/ModalBase/index.ts new file mode 100644 index 0000000000..639a988486 --- /dev/null +++ b/packages/plasma-hope/src/components/ModalBase/index.ts @@ -0,0 +1,2 @@ +export { ModalBase } from '@salutejs/plasma-core'; +export type { ModalBaseProps } from '@salutejs/plasma-core'; diff --git a/packages/plasma-hope/src/components/PopupBase/index.ts b/packages/plasma-hope/src/components/PopupBase/index.ts index be803c11df..9636e9486c 100644 --- a/packages/plasma-hope/src/components/PopupBase/index.ts +++ b/packages/plasma-hope/src/components/PopupBase/index.ts @@ -1,2 +1,4 @@ +export { PopupBaseProvider, PopupBaseContext } from '@salutejs/plasma-core'; + export { PopupBase } from '@salutejs/plasma-core'; export type { PopupBaseProps, PopupBasePlacement } from '@salutejs/plasma-core'; diff --git a/packages/plasma-hope/src/index.ts b/packages/plasma-hope/src/index.ts index 4e857428c4..dc90fd1178 100644 --- a/packages/plasma-hope/src/index.ts +++ b/packages/plasma-hope/src/index.ts @@ -12,6 +12,7 @@ export * from './components/Grid'; export * from './components/Image'; export * from './components/List'; export * from './components/Modal'; +export * from './components/ModalBase'; export * from './components/Notification'; export * from './components/PaginationDots'; export * from './components/Popup'; diff --git a/packages/plasma-web/api/plasma-web.api.md b/packages/plasma-web/api/plasma-web.api.md index e10f1c54dc..68ed111577 100644 --- a/packages/plasma-web/api/plasma-web.api.md +++ b/packages/plasma-web/api/plasma-web.api.md @@ -146,6 +146,8 @@ import { MaxLinesProps } from '@salutejs/plasma-core'; import { mediaQuery } from '@salutejs/plasma-hope'; import { MediaQueryFunction } from '@salutejs/plasma-hope'; import { Modal } from '@salutejs/plasma-hope'; +import { ModalBase } from '@salutejs/plasma-hope'; +import { ModalBaseProps } from '@salutejs/plasma-hope'; import { ModalProps } from '@salutejs/plasma-hope'; import { ModalsProvider } from '@salutejs/plasma-hope'; import { ModalView } from '@salutejs/plasma-hope'; @@ -172,8 +174,10 @@ 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 { PopupProps } from '@salutejs/plasma-hope'; import { PreviewGallery } from '@salutejs/plasma-hope'; import { PreviewGalleryItemProps } from '@salutejs/plasma-hope'; @@ -591,6 +595,10 @@ export { MediaQueryFunction } export { Modal } +export { ModalBase } + +export { ModalBaseProps } + export { ModalProps } export { ModalsProvider } @@ -643,10 +651,14 @@ export { Popup } export { PopupBase } +export { PopupBaseContext } + export { PopupBasePlacement } export { PopupBaseProps } +export { PopupBaseProvider } + export { PopupProps } export { PreviewGallery } diff --git a/packages/plasma-web/src/components/ModalBase/ModalBase.stories.tsx b/packages/plasma-web/src/components/ModalBase/ModalBase.stories.tsx new file mode 100644 index 0000000000..41454833a4 --- /dev/null +++ b/packages/plasma-web/src/components/ModalBase/ModalBase.stories.tsx @@ -0,0 +1,139 @@ +import React from 'react'; +import styled, { createGlobalStyle } from 'styled-components'; +import { Story, Meta } from '@storybook/react'; +import { surfaceSolid02, darkOverlayBlur, overlaySoft } from '@salutejs/plasma-tokens-web'; + +import { SSRProvider } from '../SSRProvider'; +import { InSpacingDecorator } from '../../helpers'; +import { Button } from '../Button'; +import { PopupBaseProvider } from '../PopupBase'; + +import { ModalBase } from '.'; + +export default { + title: 'Controls/ModalBase', + decorators: [InSpacingDecorator], + argTypes: { + placement: { + options: [ + 'center', + 'top', + 'bottom', + 'right', + 'left', + 'top-right', + 'top-left', + 'bottom-right', + 'bottom-left', + ], + control: { + type: 'select', + }, + }, + }, +} as Meta; + +type ModalBaseStoryProps = { + placement: string; + offsetX: number; + offsetY: number; + closeOnEsc: boolean; + closeOnOverlayClick: boolean; + withBlur: boolean; +}; + +const StyledButton = styled(Button)` + margin-top: 1rem; + width: 15rem; +`; + +const StyledWrapper = styled.div` + height: 1200px; +`; + +const ModalOverlayVariables = createGlobalStyle` + body { + --plasma-modal-blur-overlay-color: ${darkOverlayBlur}; + --plasma-modal-overlay-color: ${overlaySoft}; + } +`; + +const Content = styled.div` + background: ${surfaceSolid02}; + padding: 1rem; +`; + +export const ModalBaseDemo: Story = ({ placement, offsetX, offsetY, ...rest }) => { + const [isOpenA, setIsOpenA] = React.useState(false); + const [isOpenB, setIsOpenB] = React.useState(false); + const [isOpenC, setIsOpenC] = React.useState(false); + + return ( + + + + +
+ setIsOpenA(true)} /> +
+ setIsOpenA(false)} + isOpen={isOpenA} + placement={placement} + offset={[offsetX, offsetY]} + {...rest} + > + + +
+ setIsOpenB(true)} /> +
+ setIsOpenB(false)} + isOpen={isOpenB} + placement="left" + offset={[offsetX, offsetY]} + {...rest} + > + + +
+ setIsOpenC(true)} /> +
+ setIsOpenC(false)} + isOpen={isOpenC} + placement="top" + offset={[offsetX, offsetY]} + {...rest} + > + + + <>Content + + +
+
+
+
+
+
+
+ ); +}; + +ModalBaseDemo.args = { + placement: 'center', + withBlur: false, + closeOnEsc: true, + closeOnOverlayClick: true, + offsetX: 0, + offsetY: 0, +}; diff --git a/packages/plasma-web/src/components/ModalBase/index.ts b/packages/plasma-web/src/components/ModalBase/index.ts new file mode 100644 index 0000000000..3d4e2af155 --- /dev/null +++ b/packages/plasma-web/src/components/ModalBase/index.ts @@ -0,0 +1,2 @@ +export { ModalBase } from '@salutejs/plasma-hope'; +export type { ModalBaseProps } from '@salutejs/plasma-hope'; diff --git a/packages/plasma-web/src/components/PopupBase/PopupBase.stories.tsx b/packages/plasma-web/src/components/PopupBase/PopupBase.stories.tsx index c12cd1c9a5..d97b42026b 100644 --- a/packages/plasma-web/src/components/PopupBase/PopupBase.stories.tsx +++ b/packages/plasma-web/src/components/PopupBase/PopupBase.stories.tsx @@ -7,7 +7,7 @@ import { SSRProvider } from '../SSRProvider'; import { InSpacingDecorator } from '../../helpers'; import { Button } from '../Button'; -import { PopupBase } from '.'; +import { PopupBase, PopupBaseProvider } from '.'; export default { title: 'Controls/PopupBase', @@ -73,25 +73,27 @@ export const PopupBaseDemo: Story = ({ placement, offsetX, return ( -
- setIsOpenA(true)} /> - setIsOpenB(true)} /> -
- - - - <>Content - - - - <>Frame - - - - - <>Content - - + +
+ setIsOpenA(true)} /> + setIsOpenB(true)} /> +
+ + + + <>Content + + + + <>Frame + + + + + <>Content + + +
); diff --git a/packages/plasma-web/src/components/PopupBase/index.ts b/packages/plasma-web/src/components/PopupBase/index.ts index 312f45f3bf..8a274eead0 100644 --- a/packages/plasma-web/src/components/PopupBase/index.ts +++ b/packages/plasma-web/src/components/PopupBase/index.ts @@ -1,2 +1,4 @@ +export { PopupBaseProvider, PopupBaseContext } from '@salutejs/plasma-hope'; + export { PopupBase } from '@salutejs/plasma-hope'; export type { PopupBaseProps, PopupBasePlacement } from '@salutejs/plasma-hope'; diff --git a/packages/plasma-web/src/index.ts b/packages/plasma-web/src/index.ts index 1051adfec3..33024424cd 100644 --- a/packages/plasma-web/src/index.ts +++ b/packages/plasma-web/src/index.ts @@ -14,6 +14,7 @@ export * from './components/Image'; export * from './components/Link'; export * from './components/List'; export * from './components/Modal'; +export * from './components/ModalBase'; export * from './components/Notification'; export * from './components/PaginationDots'; export * from './components/Popup'; From e052d202b8943a8b177d9ab3a18ff500b718aeca Mon Sep 17 00:00:00 2001 From: Ivan Kudryavtsev Date: Thu, 28 Sep 2023 12:46:18 +0300 Subject: [PATCH 2/2] 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';