Skip to content

Commit

Permalink
feat(plasma-core): focusTrap, arrow and offset for Popover
Browse files Browse the repository at this point in the history
  • Loading branch information
kayman233 committed Sep 20, 2023
1 parent 376ddc1 commit 6763e7d
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 70 deletions.
9 changes: 9 additions & 0 deletions packages/plasma-b2c/api/plasma-b2c.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ import { ParagraphText2 } from '@salutejs/plasma-hope';
import { PickOptional } from '@salutejs/plasma-core';
import { PinProps } from '@salutejs/plasma-core';
import { Placement } from '@salutejs/plasma-hope';
import { Popover } from '@salutejs/plasma-hope';
import { PopoverPlacement } from '@salutejs/plasma-hope';
import { PopoverProps } from '@salutejs/plasma-hope';
import { Popup } from '@salutejs/plasma-hope';
import { PopupProps } from '@salutejs/plasma-hope';
import { PreviewGallery } from '@salutejs/plasma-hope';
Expand Down Expand Up @@ -598,6 +601,12 @@ export { PinProps }

export { Placement }

export { Popover }

export { PopoverPlacement }

export { PopoverProps }

export { Popup }

export { PopupProps }
Expand Down
97 changes: 97 additions & 0 deletions packages/plasma-b2c/src/components/Popover/Popover.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { useRef } from 'react';
import styled from 'styled-components';
import { Story, Meta } from '@storybook/react';
import { surfaceSolid03 } from '@salutejs/plasma-tokens-web';
import { InSpacingDecorator, disableProps } from '@salutejs/plasma-sb-utils';

import { Button } from '../Button';

import { Popover, PopoverProps, PopoverPlacement } from '.';

const placements: Array<PopoverPlacement> = ['top', 'bottom', 'right', 'left', 'auto'];

export default {
title: 'Controls/Popover',
decorators: [InSpacingDecorator],
} as Meta;

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

const StyledContent = styled.div`
background: ${surfaceSolid03};
padding: 1rem;
`;

export const Live: Story<PopoverProps & { skidding?: number; distance?: number }> = ({
skidding = 0,
distance = 0,
...args
}) => {
const [isOpen, setIsOpen] = React.useState(false);

return (
<>
<Button style={{ margin: '0 16' }} tabIndex={0} onClick={() => setIsOpen(true)}>
Open
</Button>
<Popover
isOpen={isOpen}
onToggle={(is) => setIsOpen(is)}
role="presentation"
id="popover"
target={<div>Trigger</div>}
arrow={<StyledArrow />}
offset={[skidding, distance]}
{...args}
>
<StyledContent>
<>Content</>
<Button onClick={() => setIsOpen(false)}>close1</Button>
<Button onClick={() => setIsOpen(false)}>close2</Button>
</StyledContent>
</Popover>
</>
);
};

Live.args = {
placement: 'bottom',
trigger: 'click',
closeOnOverlayClick: true,
closeOnEsc: true,
isFocusTrapped: false,
skidding: 0,
distance: 6,
};

Live.argTypes = {
placement: {
options: placements,
control: {
type: 'select',
},
},
trigger: {
options: ['click', 'hover'],
control: {
type: 'select',
},
},
...disableProps(['isOpen']),
};
2 changes: 2 additions & 0 deletions packages/plasma-b2c/src/components/Popover/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Popover } from '@salutejs/plasma-hope';
export type { PopoverProps, PopoverPlacement } from '@salutejs/plasma-hope';
1 change: 1 addition & 0 deletions packages/plasma-b2c/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/PreviewGallery';
export * from './components/Price';
export * from './components/Progress';
Expand Down
11 changes: 9 additions & 2 deletions packages/plasma-core/api/plasma-core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { InterpolationFunction } from 'styled-components';
import { MutableRefObject } from 'react';
import { default as React_2 } from 'react';
import { ReactNode } from 'react';
import { RefObject } from 'react';
import { spacing } from '@salutejs/plasma-typo';
import { SpacingProps } from '@salutejs/plasma-typo';
import { SpacingProps as SpacingProps_2 } from '@salutejs/plasma-typo/lib/cjs/mixins/applySpacing';
Expand Down Expand Up @@ -824,12 +825,15 @@ export type PopoverPlacement = PopoverBasicPlacement | 'auto';

// @public (undocumented)
export interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
arrow?: ReactNode;
children?: ReactNode;
closeOnOverlayClick: boolean;
closeOnEsc?: boolean;
closeOnOverlayClick?: boolean;
isFocusTrapped?: boolean;
isOpen?: boolean;
offset?: [number, number];
onToggle?: (isOpen: boolean, event: SyntheticEvent | Event) => void;
placement?: PopoverPlacement | Array<PopoverBasicPlacement>;
showArrow?: boolean;
target?: ReactNode;
trigger: 'hover' | 'click';
}
Expand Down Expand Up @@ -1260,6 +1264,9 @@ export type UseCarouselOptions = Pick<CarouselProps, 'index' | 'axis' | 'detectA
// @public (undocumented)
export function useDebouncedFunction(func: (...args: any) => any, delay: number, cleanUp?: boolean): (...args: any[]) => void;

// @public
export const useFocusTrap: (active?: boolean, firstFocusSelector?: string | RefObject<HTMLElement> | undefined, focusAfterNode?: RefObject<HTMLElement> | undefined) => (instance: HTMLElement | null) => void;

// Warning: (ae-forgotten-export) The symbol "UseForkRefHook" needs to be exported by the entry point index.d.ts
//
// @public
Expand Down
114 changes: 65 additions & 49 deletions packages/plasma-core/src/components/Popover/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import styled from 'styled-components';
import { usePopper } from 'react-popper';
import PopperJS from '@popperjs/core';

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

const ESCAPE_KEYCODE = 27;

export type PopoverBasicPlacement = 'top' | 'bottom' | 'right' | 'left';
export type PopoverPlacement = PopoverBasicPlacement | 'auto';
Expand All @@ -23,25 +25,37 @@ export interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
*/
placement?: PopoverPlacement | Array<PopoverBasicPlacement>;
/**
* Стрелка над элементом показывается или нет.
* Отступ окна относительно элемента, у которого оно вызывано
*/
showArrow?: boolean;
offset?: [number, number];
/**
* Элемент, при нажатии на который произойдет вызов всплывающего окна.
* Элемент, рядом с котором произойдет вызов всплывающего окна.
*/
target?: ReactNode;
/**
* Стрелка над элементом.
*/
arrow?: ReactNode;
/**
* Контент всплывающего окна.
*/
children?: ReactNode;
/**
* Блокировать ли фокус на вспывающем окне.
*/
isFocusTrapped?: boolean;
/**
* Событие сворачивания/разворачивания всплывающего окна.
*/
onToggle?: (isOpen: boolean, event: SyntheticEvent | Event) => void;
/**
* Закрывать окно при нажатии вне области окна(по умолчанию true),
*/
closeOnOverlayClick: boolean;
closeOnOverlayClick?: boolean;
/**
* Закрывать окно при нажатии ESC(по умолчанию true),
*/
closeOnEsc?: boolean;
}

const StyledRoot = styled.div`
Expand All @@ -50,47 +64,26 @@ const StyledRoot = styled.div`
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} {
/* stylelint-disable selector-max-id */
&[data-popper-placement^='top'] > #popover-arrow {
bottom: -0.25rem;
}
&[data-popper-placement^='bottom'] > ${StyledArrow} {
&[data-popper-placement^='bottom'] > #popover-arrow {
top: -0.25rem;
}
&[data-popper-placement^='left'] > ${StyledArrow} {
&[data-popper-placement^='left'] > #popover-arrow {
right: -0.25rem;
}
&[data-popper-placement^='right'] > ${StyledArrow} {
&[data-popper-placement^='right'] > #popover-arrow {
left: -0.25rem;
}
padding: var(--plasma-popup-padding);
margin: var(--plasma-popup-margin);
width: var(--plasma-popup-width);
`;

export const getPlacement = (placement: PopoverPlacement) => {
Expand All @@ -113,9 +106,12 @@ export const Popover = memo<PopoverProps & RefAttributes<HTMLDivElement>>(
children,
isOpen,
trigger,
showArrow = false,
arrow,
placement = 'auto',
offset = [0, 0],
isFocusTrapped = false,
closeOnOverlayClick = true,
closeOnEsc = true,
onToggle,
...rest
},
Expand All @@ -125,6 +121,10 @@ export const Popover = memo<PopoverProps & RefAttributes<HTMLDivElement>>(
const popoverRef = useRef<HTMLDivElement | null>(null);
const handleRef = useForkRef<HTMLDivElement>(rootRef, outerRootRef);

const trapRef = useFocusTrap(isOpen && isFocusTrapped);

const popoverForkRef = useForkRef<HTMLDivElement>(popoverRef, trapRef);

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

const isAutoArray = Array.isArray(placement);
Expand All @@ -133,6 +133,7 @@ export const Popover = memo<PopoverProps & RefAttributes<HTMLDivElement>>(
const { styles, attributes, forceUpdate } = usePopper(rootRef.current, popoverRef.current, {
placement: getPlacement(isAutoArray ? 'auto' : (placement as PopoverPlacement)),
modifiers: [
{ name: 'offset', options: { offset: [offset[0], offset[1]] } },
{
name: 'flip',
enabled: isAuto,
Expand All @@ -146,26 +147,32 @@ export const Popover = memo<PopoverProps & RefAttributes<HTMLDivElement>>(
],
});

const onEscape = useCallback(
(event: KeyboardEvent) => {
if (isOpen && closeOnEsc && event.keyCode === ESCAPE_KEYCODE) {
onToggle?.(false, event);
}
},
[closeOnEsc, isOpen, onToggle],
);

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 (isOpen && closeOnOverlayClick && onToggle) {
const targetIsRoot = event.target === rootRef.current;
const rootHasTarget = rootRef.current?.contains(event.target as Element);

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

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);

Expand Down Expand Up @@ -216,10 +223,14 @@ export const Popover = memo<PopoverProps & RefAttributes<HTMLDivElement>>(
useEffect(() => {
document.addEventListener('click', onDocumentClick);
return () => document.removeEventListener('click', onDocumentClick);
}, []);
}, [closeOnOverlayClick, isOpen, onToggle]);

useEffect(() => {
window.addEventListener('keydown', onEscape);
return () => window.removeEventListener('keydown', onEscape);
}, [closeOnEsc, isOpen, onToggle]);

useEffect(() => {
console.log('effect', isOpen);
if (!isOpen || !forceUpdate) {
return;
}
Expand All @@ -233,8 +244,6 @@ export const Popover = memo<PopoverProps & RefAttributes<HTMLDivElement>>(
Promise.resolve().then(forceUpdate);
}, [isOpen, forceUpdate]);

console.log(isOpen);

return (
<StyledRoot
ref={handleRef}
Expand All @@ -249,11 +258,18 @@ export const Popover = memo<PopoverProps & RefAttributes<HTMLDivElement>>(
{children && (
<StyledPopover
{...attributes.popper}
ref={popoverRef}
ref={popoverForkRef}
style={{ ...styles.popper, ...{ display: isOpen ? 'block' : 'none' } }}
>
{showArrow && (
<StyledArrow ref={setArrowElement} style={styles.arrow} {...attributes.arrow} />
{arrow && (
<div
id="popover-arrow"
ref={setArrowElement}
style={styles.arrow}
{...attributes.arrow}
>
{arrow}
</div>
)}
{children}
</StyledPopover>
Expand Down
Loading

0 comments on commit 6763e7d

Please sign in to comment.