Skip to content

Commit

Permalink
feat(plasma-new-hope): Popup added
Browse files Browse the repository at this point in the history
  • Loading branch information
kayman233 committed Oct 25, 2023
1 parent 159e7cb commit 069ec79
Show file tree
Hide file tree
Showing 15 changed files with 979 additions and 0 deletions.
53 changes: 53 additions & 0 deletions packages/plasma-new-hope/src/components/Popup/Popup.hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useEffect, useState } from 'react';

import { PopupAnimationInfo, PopupHookArgs } from './Popup.types';
import { usePopupContext } from './PopupContext';

// Хук для поключения анимации
export const usePopupAnimation = (): PopupAnimationInfo => {
const [endAnimation, setEndAnimation] = useState<boolean>(false);
const [endTransition, setEndTransition] = useState<boolean>(false);

return { endAnimation, endTransition, setEndTransition, setEndAnimation };
};

// Хук для внутреннего состояния, необходимого для правильного отображения вложенных окон, а также для анимации
export const usePopup = ({ isOpen, id, popupInfo, withAnimation }: PopupHookArgs) => {
const [isVisible, setVisible] = useState<boolean>(false);
const popupController = usePopupContext();
const animationInfo = usePopupAnimation();

// для использования transition в качестве анимации
useEffect(() => {
if (withAnimation) {
animationInfo?.setEndTransition(animationInfo && (!isVisible || animationInfo?.endAnimation));
}
}, [animationInfo?.endAnimation, isVisible]);

// сначала добавление/удаление из контекста, и только после этого отображение/скрытие
useEffect(() => {
// при первом открытии
if (isOpen && !isVisible) {
popupController.register({ id, ...popupInfo });
setVisible(true);
animationInfo?.setEndAnimation(false);
return;
}

if (isOpen || !isVisible) {
return;
}

// если есть анимация - закрытие по окончании анимации
if (withAnimation) {
animationInfo?.setEndAnimation(true);
return;
}

// иначе обычное закрытие
popupController.unregister(id);
setVisible(false);
}, [isOpen, isVisible, animationInfo]);

return { isVisible, setVisible, animationInfo };
};
22 changes: 22 additions & 0 deletions packages/plasma-new-hope/src/components/Popup/Popup.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { styled } from '@linaria/react';

import { PopupRootContainerProps } from './Popup.types';
import { DEFAULT_Z_INDEX } from './Popup.utils';

export const StyledPortal = styled.div``;

export const PopupView = styled.div`
position: relative;
max-width: 100%;
pointer-events: all;
`;

export const PopupRootContainer = styled.div<PopupRootContainerProps>`
position: ${(props) => (props?.frame === 'document' ? 'fixed' : 'absolute')};
z-index: ${(props) => props?.zIndex || DEFAULT_Z_INDEX};
left: ${(props) => props?.position.left || ''};
right: ${(props) => props?.position.right || ''};
top: ${(props) => props?.position.top || ''};
bottom: ${(props) => props?.position.bottom || ''};
transform: ${(props) => props?.position.transform || ''};
`;
179 changes: 179 additions & 0 deletions packages/plasma-new-hope/src/components/Popup/Popup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import React, { forwardRef, useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import { useForkRef, useUniqId } from '@salutejs/plasma-core';

import { RootProps } from '../../engines/types';

import { BasicPopupPlacement, PopupPlacement, PopupPositionType, PopupProps } from './Popup.types';
import { POPUP_PORTAL_ID } from './PopupContext';
import { PopupRoot } from './PopupRoot';
import { usePopup } from './Popup.hooks';

export const handlePosition = (
placement: PopupPlacement,
offset: [number, number] | [string, string],
): PopupPositionType => {
let x = '0rem';
let y = '0rem';
if (offset) {
const [_x, _y] = offset;
x = typeof _x === 'number' ? `${_x}rem` : _x;
y = typeof _y === 'number' ? `${_y}rem` : _y;
}

if (!placement || placement === 'center') {
return {
left: `calc(50% + ${x})`,
top: `calc(50% - ${y})`,
transform: 'translate(-50%, -50%)',
};
}

let left;
let right;
let top;
let bottom;
let transform;
const placements = placement.split('-') as BasicPopupPlacement[];

placements.forEach((placement: BasicPopupPlacement) => {
switch (placement) {
case 'left':
left = x;
break;
case 'right':
right = x;
break;
case 'top':
top = y;
break;
case 'bottom':
bottom = y;
break;
default:
break;
}
});

const isCenteredX = left === undefined && right === undefined;
const isCenteredY = top === undefined && bottom === undefined;

if (isCenteredX) {
transform = 'translateX(-50%)';
}

if (isCenteredY) {
transform = 'translateY(-50%)';
}

return {
left: isCenteredX ? `calc(50% + ${x})` : left,
right,
top: isCenteredY ? `calc(50% - ${y})` : top,
bottom,
transform,
};
};

/**
* Базовый копмонент Popup.
*/
export const popupRoot = (Root: RootProps<HTMLDivElement, PopupProps>) =>
forwardRef<HTMLDivElement, PopupProps>(
(
{
id,
isOpen = false,
placement = 'center',
offset = [0, 0],
frame = 'document',
children,
overlay,
role,
zIndex,
popupInfo,
withAnimation = false,
...rest
},
outerRootRef,
) => {
const uniqId = useUniqId();
const innerId = id || uniqId;

const { isVisible, animationInfo, setVisible } = usePopup({
isOpen,
id: innerId,
popupInfo,
withAnimation,
});

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

const innerRef = useForkRef<HTMLDivElement>(contentRef, outerRootRef);

const [, forceRender] = useState(false);

useEffect(() => {
let portal = document.getElementById(POPUP_PORTAL_ID);

if (typeof frame !== 'string' && frame && frame.current) {
portal = frame.current;
}

if (!portal) {
portal = document.createElement('div');
portal.setAttribute('id', POPUP_PORTAL_ID);
if (typeof frame === 'string' && frame !== 'document') {
document.getElementById(frame)?.appendChild(portal);
} else {
document.body.appendChild(portal);
}
}

portalRef.current = portal;

/**
* Изменение стейта нужно для того, чтобы Popup
* отобразился после записи DOM элемента в portalRef.current
*/
forceRender(true);
}, []);

if (!isVisible && !isOpen) {
return null;
}

return (
<>
{portalRef.current &&
ReactDOM.createPortal(
<Root {...rest}>
{overlay}
<PopupRoot
id={innerId}
ref={innerRef}
position={handlePosition(placement, offset)}
frame={frame}
zIndex={zIndex}
animationInfo={animationInfo}
setVisible={setVisible}
>
{children}
</PopupRoot>
</Root>,
portalRef.current,
)}
</>
);
},
);

export const popupConfig = {
name: 'Popup',
tag: 'div',
layout: popupRoot,
base: '',
variations: {},
defaults: {},
};
89 changes: 89 additions & 0 deletions packages/plasma-new-hope/src/components/Popup/Popup.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
export type BasicPopupPlacement = 'center' | 'top' | 'bottom' | 'right' | 'left';
export type MixedPopupPlacement = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
export type PopupPlacement = BasicPopupPlacement | MixedPopupPlacement;

export interface PopupInfo {
id: string;
info?: Object;
}

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

export interface PopupProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* Отображение Popup.
*/
isOpen?: boolean;
/* Позиция на экране
* center - по умолчанию
* left, right, top, bottom и их комбинации
*/
placement?: PopupPlacement;
/* Смещение относительно текущей позиции.
* (x, y) - [number, number], [string, string].
* При передаче number расчёт в rem.
*/
offset?: [number, number] | [string, string];
/**
* В каком контейнере позиционируется(по умолчанию document).
*/
frame?: 'document' | string | React.RefObject<HTMLElement>;
/**
* Содержимое Popup.
*/
children?: React.ReactNode;
/**
* Соседний элемент для окна в портале.
*/
overlay?: React.ReactNode;
/**
* Значение z-index для Popup.
*/
zIndex?: string;
/**
* Дополнительная информация для программного взаимодействия с окном через контекст.
*/
popupInfo?: PopupInfo;
/**
* Использовать ли анимацию.
*/
withAnimation?: boolean;
}
export interface PopupAnimationInfo {
endAnimation: boolean;
setEndAnimation: React.Dispatch<React.SetStateAction<boolean>>;
endTransition: boolean;
setEndTransition: React.Dispatch<React.SetStateAction<boolean>>;
}

export interface PopupPositionType {
left?: string;
right?: string;
top?: string;
bottom?: string;
transform?: string;
}

export interface PopupRootProps extends Omit<PopupProps, 'isOpen' | 'overlay'> {
id: string;
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
position: PopupPositionType;
/**
* Данные из хука usePopupAnimation.
*/
animationInfo?: PopupAnimationInfo;
}

export interface PopupHookArgs extends Pick<PopupProps, 'isOpen' | 'popupInfo' | 'withAnimation'> {
id: string;
}

export interface PopupRootContainerProps extends Omit<PopupProps, 'isOpen' | 'overlay'> {
endTransition?: boolean;
endAnimation?: boolean;
position: PopupPositionType;
}
6 changes: 6 additions & 0 deletions packages/plasma-new-hope/src/components/Popup/Popup.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Класс корневого компонента PopupRoot: `popup-base-root`
*/
export const popupRootClass = 'popup-base-root';

export const DEFAULT_Z_INDEX = 9000;
Loading

0 comments on commit 069ec79

Please sign in to comment.