Skip to content

Commit

Permalink
feat(plasma-web, plasma-b2c): Popover component
Browse files Browse the repository at this point in the history
  • Loading branch information
kayman233 committed Sep 19, 2023
1 parent 46487cb commit b847cfe
Show file tree
Hide file tree
Showing 10 changed files with 372 additions and 0 deletions.
20 changes: 20 additions & 0 deletions packages/plasma-core/api/plasma-core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,26 @@ export interface PinProps {
}

// @public
export const Popover: React_2.NamedExoticComponent<PopoverProps & React_2.RefAttributes<HTMLDivElement>>;

// Warning: (ae-forgotten-export) The symbol "PopoverBasicPlacement" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export type PopoverPlacement = PopoverBasicPlacement | 'auto';

// @public (undocumented)
export interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
children?: ReactNode;
closeOnOverlayClick: boolean;
isOpen?: boolean;
onToggle?: (isOpen: boolean, event: SyntheticEvent | Event) => void;
placement?: PopoverPlacement | Array<PopoverBasicPlacement>;
showArrow?: boolean;
target?: ReactNode;
trigger: 'hover' | 'click';
}

// @public @deprecated
export const Popup: React_2.NamedExoticComponent<PopupProps & React_2.RefAttributes<HTMLDivElement>>;

// @public (undocumented)
Expand Down
265 changes: 265 additions & 0 deletions packages/plasma-core/src/components/Popover/Popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
import React, { memo, useRef, useCallback, useEffect, useState } from 'react';
import type { HTMLAttributes, ReactNode, RefAttributes, SyntheticEvent } from 'react';
import styled from 'styled-components';
import { usePopper } from 'react-popper';
import PopperJS from '@popperjs/core';

import { useForkRef } from '../../hooks';

export type PopoverBasicPlacement = 'top' | 'bottom' | 'right' | 'left';
export type PopoverPlacement = PopoverBasicPlacement | 'auto';

export interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
/**
* Всплывающее окно раскрыто или нет.
*/
isOpen?: boolean;
/**
* Способ всплывающего окна - наведение или клик мышью.
*/
trigger: 'hover' | 'click';
/**
* Расположение всплывающего окна. По умолчанию "auto"
*/
placement?: PopoverPlacement | Array<PopoverBasicPlacement>;
/**
* Стрелка над элементом показывается или нет.
*/
showArrow?: boolean;
/**
* Элемент, при нажатии на который произойдет вызов всплывающего окна.
*/
target?: ReactNode;
/**
* Контент всплывающего окна.
*/
children?: ReactNode;
/**
* Событие сворачивания/разворачивания всплывающего окна.
*/
onToggle?: (isOpen: boolean, event: SyntheticEvent | Event) => void;
/**
* Закрывать окно при нажатии вне области окна(по умолчанию true),
*/
closeOnOverlayClick: boolean;
}

const StyledRoot = styled.div`
position: relative;
box-sizing: border-box;
display: inline-flex;
`;

const StyledArrow = styled.div`
visibility: hidden;
&,
&::before {
position: absolute;
width: 0.5rem;
height: 0.5rem;
background: inherit;
}
&::before {
visibility: visible;
content: '';
transform: rotate(45deg);
}
`;

const StyledPopover = styled.div`
position: absolute;
z-index: 1;
&[data-popper-placement^='top'] > ${StyledArrow} {
bottom: -0.25rem;
}
&[data-popper-placement^='bottom'] > ${StyledArrow} {
top: -0.25rem;
}
&[data-popper-placement^='left'] > ${StyledArrow} {
right: -0.25rem;
}
&[data-popper-placement^='right'] > ${StyledArrow} {
left: -0.25rem;
}
padding: var(--plasma-popup-padding);
margin: var(--plasma-popup-margin);
width: var(--plasma-popup-width);
`;

export const getPlacement = (placement: PopoverPlacement) => {
return `${placement}-start` as PopperJS.Placement;
};

const getAutoPlacements = (placements?: PopoverPlacement[]) => {
return (placements || []).map((placement) => getPlacement(placement));
};

/**
* Всплывающее окно с возможностью позиционирования
* и вызова по клику либо ховеру.
*/
export const Popover = memo<PopoverProps & RefAttributes<HTMLDivElement>>(
React.forwardRef<HTMLDivElement, PopoverProps>(
(
{
target,
children,
isOpen,
trigger,
showArrow = false,
placement = 'auto',
closeOnOverlayClick = true,
onToggle,
...rest
},
outerRootRef,
) => {
const rootRef = useRef<HTMLDivElement | null>(null);
const popoverRef = useRef<HTMLDivElement | null>(null);
const handleRef = useForkRef<HTMLDivElement>(rootRef, outerRootRef);

const [arrowElement, setArrowElement] = useState<HTMLSpanElement | null>(null);

const isAutoArray = Array.isArray(placement);
const isAuto = isAutoArray || placement === 'auto';

const { styles, attributes, forceUpdate } = usePopper(rootRef.current, popoverRef.current, {
placement: getPlacement(isAutoArray ? 'auto' : (placement as PopoverPlacement)),
modifiers: [
{
name: 'flip',
enabled: isAuto,
options: {
allowedAutoPlacements: getAutoPlacements(
isAutoArray ? (placement as PopoverPlacement[]) : [],
),
},
},
{ name: 'arrow', options: { element: arrowElement } },
],
});

const onDocumentClick = useCallback(
(event: MouseEvent) => {
console.log('closeOnOverlayClick', closeOnOverlayClick);
if (!closeOnOverlayClick) {
return;
}
const targetIsRoot = event.target === rootRef.current;
const rootHasTarget = rootRef.current?.contains(event.target as Element);

if (!targetIsRoot && !rootHasTarget) {
onToggle?.(false, event);
}
},
[onToggle, closeOnOverlayClick],
);

const onClick = useCallback<React.MouseEventHandler>(
(event) => {
if (trigger === 'click') {
console.log('popover click');
const targetIsPopover = event.target === popoverRef.current;
const rootHasTarget = popoverRef.current?.contains(event.target as Element);

if (!targetIsPopover && !rootHasTarget) {
onToggle?.(!isOpen, event);
}
}
},
[trigger, isOpen, onToggle],
);

const onMouseEnter = useCallback<React.MouseEventHandler>(
(event) => {
if (trigger === 'hover') {
onToggle?.(true, event);
}
},
[trigger, onToggle],
);

const onMouseLeave = useCallback<React.MouseEventHandler>(
(event) => {
if (trigger === 'hover') {
onToggle?.(false, event);
}
},
[trigger, onToggle],
);

const onFocus = useCallback<React.FocusEventHandler>(
(event) => {
if (trigger === 'hover') {
onToggle?.(true, event);
}
},
[trigger, onToggle],
);

const onBlur = useCallback<React.FocusEventHandler>(
(event) => {
if (trigger === 'hover') {
onToggle?.(false, event);
}
},
[trigger, onToggle],
);

useEffect(() => {
document.addEventListener('click', onDocumentClick);
return () => document.removeEventListener('click', onDocumentClick);
}, []);

useEffect(() => {
console.log('effect', isOpen);
if (!isOpen || !forceUpdate) {
return;
}

/*
* INFO: Метод forceUpdate содержит в себе flushSync и приводит
* к повторному рендеру компонента, который уже находится в процессе рендера.
* Данный хак, нужен для того, чтобы это поведение избежать и перенаправить
* вызов метода в очередь микрозадач.
*/
Promise.resolve().then(forceUpdate);
}, [isOpen, forceUpdate]);

console.log(isOpen);

return (
<StyledRoot
ref={handleRef}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onFocus={onFocus}
onBlur={onBlur}
{...rest}
>
{target}
{children && (
<StyledPopover
{...attributes.popper}
ref={popoverRef}
style={{ ...styles.popper, ...{ display: isOpen ? 'block' : 'none' } }}
>
{showArrow && (
<StyledArrow ref={setArrowElement} style={styles.arrow} {...attributes.arrow} />
)}
{children}
</StyledPopover>
)}
</StyledRoot>
);
},
),
);
2 changes: 2 additions & 0 deletions packages/plasma-core/src/components/Popover/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Popover } from './Popover';
export type { PopoverProps, PopoverPlacement } from './Popover';
1 change: 1 addition & 0 deletions packages/plasma-core/src/components/Popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const getAutoPlacements = (placements?: PopupPlacement[]) => {
/**
* Всплывающее окно с возможностью позиционирования
* и вызова по клику либо ховеру.
* @deprecated Используйте Popover
*/
export const Popup = memo<PopupProps & RefAttributes<HTMLDivElement>>(
React.forwardRef<HTMLDivElement, PopupProps>(
Expand Down
1 change: 1 addition & 0 deletions packages/plasma-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './components/Image';
export * from './components/Input';
export * from './components/PaginationDots';
export * from './components/Popup';
export * from './components/Popover';
export * from './components/Price';
export * from './components/RadioGroup';
export * from './components/Skeleton';
Expand Down
9 changes: 9 additions & 0 deletions packages/plasma-hope/api/plasma-hope.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ import { ParagraphText1 } from '@salutejs/plasma-core';
import { ParagraphText2 } from '@salutejs/plasma-core';
import { PickOptional } from '@salutejs/plasma-core';
import { PinProps } from '@salutejs/plasma-core';
import { Popover } from '@salutejs/plasma-core';
import { PopoverPlacement } from '@salutejs/plasma-core';
import { PopoverProps } from '@salutejs/plasma-core';
import { Popup } from '@salutejs/plasma-core';
import { PopupProps } from '@salutejs/plasma-core';
import type { PriceProps as PriceProps_2 } from '@salutejs/plasma-core';
Expand Down Expand Up @@ -994,6 +997,12 @@ export { PinProps }
// @public (undocumented)
export type Placement = BasePlacement | VariationPlacement;

export { Popover }

export { PopoverPlacement }

export { PopoverProps }

export { Popup }

export { PopupProps }
Expand Down
2 changes: 2 additions & 0 deletions packages/plasma-hope/src/components/Popover/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Popover } from '@salutejs/plasma-core';
export type { PopoverProps, PopoverPlacement } from '@salutejs/plasma-core';
1 change: 1 addition & 0 deletions packages/plasma-hope/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export * from './components/Modal';
export * from './components/Notification';
export * from './components/PaginationDots';
export * from './components/Popup';
export * from './components/Popover';
export * from './components/Price';
export * from './components/Progress';
export * from './components/PreviewGallery';
Expand Down
Loading

0 comments on commit b847cfe

Please sign in to comment.