-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
979 additions
and
0 deletions.
There are no files selected for viewing
53 changes: 53 additions & 0 deletions
53
packages/plasma-new-hope/src/components/Popup/Popup.hooks.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
22
packages/plasma-new-hope/src/components/Popup/Popup.styles.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
179
packages/plasma-new-hope/src/components/Popup/Popup.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
89
packages/plasma-new-hope/src/components/Popup/Popup.types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.