diff --git a/packages/plasma-b2c/api/plasma-b2c.api.md b/packages/plasma-b2c/api/plasma-b2c.api.md index 30a570ab87..a47fbc57f1 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'; @@ -174,6 +176,9 @@ import { Popup } from '@salutejs/plasma-hope'; import { PopupBase } 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'; @@ -260,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'; @@ -591,6 +597,10 @@ export { MediaQueryFunction } export { Modal } +export { ModalBase } + +export { ModalBaseProps } + export { ModalProps } export { ModalsProvider } @@ -647,6 +657,12 @@ export { PopupBasePlacement } export { PopupBaseProps } +export { PopupBaseProvider } + +export { PopupContextType } + +export { PopupInfo } + export { PopupProps } export { PreviewGallery } @@ -861,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 new file mode 100644 index 0000000000..5fc3f27646 --- /dev/null +++ b/packages/plasma-b2c/src/components/ModalBase/ModalBase.stories.tsx @@ -0,0 +1,140 @@ +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; +`; + +// TODO: новый отдельный оверлей #778 +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..16f402bad9 100644 --- a/packages/plasma-b2c/src/components/PopupBase/index.ts +++ b/packages/plasma-b2c/src/components/PopupBase/index.ts @@ -1,2 +1,5 @@ +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-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..4cc9e68939 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,7 @@ 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 "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 @@ -857,11 +872,36 @@ export interface PopupBaseProps extends React_2.HTMLAttributes { isOpen: boolean; // (undocumented) offset?: [number | string, number | string]; + overlay?: React_2.ReactNode; // (undocumented) placement?: PopupBasePlacement; + popupInfo?: PopupInfo; zIndex?: string; } +// @public (undocumented) +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; @@ -1307,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 new file mode 100644 index 0000000000..4d90e2c746 --- /dev/null +++ b/packages/plasma-core/src/components/ModalBase/ModalBase.tsx @@ -0,0 +1,180 @@ +import React, { useCallback, FC, useEffect } from 'react'; +import styled, { createGlobalStyle, css } from 'styled-components'; + +import { useFocusTrap, useUniqId } from '../../hooks'; +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 для подложки. + */ + 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; +} + +// TODO: новый отдельный оверлей #778 +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 popupController = usePopupBaseContext(); + + const trapRef = useFocusTrap(true, initialFocusRef, focusAfterRef); + + const onOverlayKeyDown = useCallback( + (event: React.MouseEvent) => { + if (!closeOnOverlayClick) { + return; + } + + if (onOverlayClick) { + onOverlayClick(event); + return; + } + + if (onClose) { + onClose(); + } + }, + [closeOnOverlayClick, onOverlayClick, onClose], + ); + + // При ESC закрывает текущее окно, если это возможно + const onKeyDown = useCallback( + (event: KeyboardEvent) => { + if (closeOnEsc && event.keyCode === ESCAPE_KEYCODE && getIdLastModal(popupController.items) === innerId) { + if (onEscKeyDown) { + onEscKeyDown(event); + return; + } + + if (onClose) { + onClose(); + } + } + }, + [onClose, onEscKeyDown, popupController, closeOnEsc], + ); + + useEffect(() => { + window.addEventListener('keydown', onKeyDown); + return () => { + window.removeEventListener('keydown', onKeyDown); + }; + }, [onClose, onEscKeyDown, popupController, closeOnEsc]); + + const modalInfo: ModalInfo = { + id: innerId, + info: { + isModal: true, + }, + }; + + return ( + <> + + + } + {...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/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..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, FC } 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 } 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,10 +33,18 @@ export interface PopupBaseProps extends React.HTMLAttributes { * Содержимое PopupBase. */ children?: React.ReactNode; + /** + * Соседний элемент для окна в портале. + */ + overlay?: React.ReactNode; /** * Значение z-index для PopupBase. */ zIndex?: string; + /** + * Дополнительная информация для программного взаимодействия с окном через контекст. + */ + popupInfo?: PopupInfo; } interface HidingProps { @@ -132,70 +140,82 @@ 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, overlay, role, zIndex, popupInfo, ...rest }, + ref, + ) => { + // Внутренее состояние, необходимое для правильного отображения вложенных окон, а также для анимации + const [isClosed, setClosed] = useState(!isOpen); + + const uniqId = useUniqId(); + const innerId = id || uniqId; + + const portalRef = useRef(null); + + const popupController = usePopupBaseContext(); + + 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); + } + + portalRef.current = portal; + + /** + * Изменение стейта нужно для того, чтобы PopupBase + * отобразился после записи DOM элемента в portalRef.current + */ + forceRender(true); + }, []); + + useEffect(() => { + // сначала добавление/удаление из контекста + if (isOpen) { + popupController.register({ id: innerId, ...popupInfo }); + } else { + popupController.unregister(innerId); + } + // затем отображение + setClosed(!isOpen); + }, [isOpen]); + + if (isClosed) { + return null; } - portalRef.current = portal; - - /** - * Изменение стейта нужно для того, чтобы PopupBase - * отобразился после записи DOM элемента в portalRef.current - */ - forceRender(true); - - return () => { - controller.unregister(innerId); - }; - }, [controller, innerId, zIndex]); - - if (isOpen) { - controller.register(innerId); - } else { - controller.unregister(innerId); - return null; - } - - return ( - <> - {portalRef.current && - ReactDOM.createPortal( - - - {children} - - , - portalRef.current, - )} - - ); -}; + return ( + <> + {portalRef.current && + ReactDOM.createPortal( + <> + {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 5b519ff416..d47450e847 100644 --- a/packages/plasma-core/src/components/PopupBase/PopupBaseContext.tsx +++ b/packages/plasma-core/src/components/PopupBase/PopupBaseContext.tsx @@ -1,27 +1,56 @@ import React, { ReactNode, useEffect } from 'react'; -/** - * Хранилище модальных окон. - */ -class PopupBaseController { - public items: string[] = []; - - public register(id: string) { - return this.items.push(id); - } - - public unregister(id: string) { - this.items.splice(this.items.indexOf(id), 1); - } +export interface PopupInfo { + id: string; + info?: Object; } -const controller = new PopupBaseController(); - export const POPOVER_PORTAL_ID = 'plasma-popup-root'; -export const PopupBaseContext = React.createContext(controller); +const items: PopupInfo[] = []; + +export interface PopupContextType { + items: PopupInfo[]; + register: (info: PopupInfo) => void; + unregister: (id: string) => void; +} + +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.'); + }, +}); + +export const usePopupBaseContext = () => React.useContext(PopupBaseContext); export const PopupBaseProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [items, setItems] = React.useState([]); + + const register = (info: PopupInfo) => { + const updatedItems = [...items]; + updatedItems.push(info); + setItems(updatedItems); + }; + + const unregister = (id: string) => { + const index = items.findIndex((item: PopupInfo) => id === item.id); + if (index === -1) { + return; + } + items.splice(index, 1); + setItems([...items]); + }; + + const context = { + items, + register, + unregister, + }; + useEffect(() => { return () => { const portal = document.createElement('div'); @@ -31,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 e9ec8717f5..326db4367d 100644 --- a/packages/plasma-core/src/components/PopupBase/index.ts +++ b/packages/plasma-core/src/components/PopupBase/index.ts @@ -1,2 +1,5 @@ +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-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..bea958f125 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'; @@ -116,6 +118,9 @@ import { Popup } from '@salutejs/plasma-core'; import { PopupBase } 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'; @@ -171,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'; @@ -898,6 +904,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; @@ -1000,6 +1010,12 @@ export { PopupBasePlacement } export { PopupBaseProps } +export { PopupBaseProvider } + +export { PopupContextType } + +export { PopupInfo } + export { PopupProps } // @public @@ -1339,6 +1355,8 @@ export { useForkRef } export { useIsomorphicLayoutEffect } +export { usePopupBaseContext } + export { useResizeObserver } export { useToast } 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..1cb192a38a 100644 --- a/packages/plasma-hope/src/components/PopupBase/index.ts +++ b/packages/plasma-hope/src/components/PopupBase/index.ts @@ -1,2 +1,5 @@ +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-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..bb0f147b66 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'; @@ -174,6 +176,9 @@ import { Popup } from '@salutejs/plasma-hope'; import { PopupBase } 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'; @@ -260,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'; @@ -591,6 +597,10 @@ export { MediaQueryFunction } export { Modal } +export { ModalBase } + +export { ModalBaseProps } + export { ModalProps } export { ModalsProvider } @@ -647,6 +657,12 @@ export { PopupBasePlacement } export { PopupBaseProps } +export { PopupBaseProvider } + +export { PopupContextType } + +export { PopupInfo } + export { PopupProps } export { PreviewGallery } @@ -861,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 new file mode 100644 index 0000000000..09757540df --- /dev/null +++ b/packages/plasma-web/src/components/ModalBase/ModalBase.stories.tsx @@ -0,0 +1,140 @@ +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; +`; + +// TODO: новый отдельный оверлей #778 +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..16f402bad9 100644 --- a/packages/plasma-web/src/components/PopupBase/index.ts +++ b/packages/plasma-web/src/components/PopupBase/index.ts @@ -1,2 +1,5 @@ +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-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';