Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
"re-resizable": "^6.9.0",
"react": "^17.*",
"react-dom": "^17.*",
"react-popper": "^2.1.0",
"react-router-dom": "^5.*",
"react-transition-group": "^4.4.1",
"react-use": "^17.2.4",
Expand Down
1 change: 1 addition & 0 deletions src/css/common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
box-sizing: border-box;
line-height: 0;
}

29 changes: 29 additions & 0 deletions src/primitives/Popper/Manager/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from "react";

export const ManagerReferenceNodeContext = React.createContext<HTMLElement>(null!);
export const ManagerReferenceNodeSetterContext = React.createContext<(elem: HTMLElement) => void>(null!);

export function Manager({ children }: React.PropsWithChildren<{}>) {
const [referenceNode, setReferenceNode] = React.useState<any>(null);

const hasUnmounted = React.useRef(false);
Comment thread
grigoriy-grisha marked this conversation as resolved.
Outdated
React.useEffect(() => {
return () => {
hasUnmounted.current = true;
};
}, []);

const handleSetReferenceNode = React.useCallback((node) => {
if (!hasUnmounted.current) {
setReferenceNode(node);
}
}, []);

return (
<ManagerReferenceNodeContext.Provider value={referenceNode}>
<ManagerReferenceNodeSetterContext.Provider value={handleSetReferenceNode}>
{children}
</ManagerReferenceNodeSetterContext.Provider>
</ManagerReferenceNodeContext.Provider>
);
}
100 changes: 100 additions & 0 deletions src/primitives/Popper/Popper/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { Ref } from "react";
import { provideRef } from "@worksolutions/react-utils";
import { Modifier, Options, Placement, PositioningStrategy, State } from "@popperjs/core/lib";

import { ManagerReferenceNodeContext } from "../Manager";

import { unwrapArray } from "../utils";
import { useVanillaPopper } from "../useVanillaPopper";

type ReferenceElement = HTMLElement;

export interface PopperArrowProps {
ref: React.Ref<any>;
style: React.CSSProperties;
}

export type PopperChildrenProps = {
ref: Ref<any>;
style: CSSStyleDeclaration;
placement: Placement;
isReferenceHidden?: boolean;
hasPopperEscaped?: boolean;
update: () => Promise<null | State>;
forceUpdate: () => void;
arrowProps: PopperArrowProps;
};

type Modifiers = Array<Partial<Modifier<any, any>>>;
export type PopperChildren = (popperChildrenProps: PopperChildrenProps) => React.ReactNode;

export type PopperProps = {
children: PopperChildren;
innerRef?: Ref<any>;
modifiers?: Modifiers;
placement?: Placement;
strategy?: PositioningStrategy;
referenceElement?: ReferenceElement;
onFirstUpdate?: (arg0: Partial<State>) => void;
};

const NOOP = () => void 0;
Comment thread
grigoriy-grisha marked this conversation as resolved.
Outdated
const NOOP_PROMISE = () => Promise.resolve(null);
const EMPTY_MODIFIERS: Modifiers = [];

export function Popper({
placement = "bottom",
strategy = "absolute",
modifiers = EMPTY_MODIFIERS,
referenceElement,
onFirstUpdate,
innerRef,
children,
}: PopperProps) {
const referenceNode = React.useContext(ManagerReferenceNodeContext);

const [popperElement, setPopperElement] = React.useState(null);
const [arrowElement, setArrowElement] = React.useState(null);

React.useEffect(() => {
provideRef(innerRef)(popperElement!);
}, [innerRef, popperElement]);

const options: Options = React.useMemo(
() => ({
placement,
strategy,
onFirstUpdate,
modifiers: [
...modifiers,
{
name: "arrow",
enabled: arrowElement != null,
options: { element: arrowElement },
},
],
}),
[placement, strategy, onFirstUpdate, modifiers, arrowElement],
);

const { state, forceUpdate, update } = useVanillaPopper(referenceElement || referenceNode, popperElement, options);

const childrenProps = React.useMemo(
() => ({
ref: setPopperElement,
style: state?.styles?.popper,
placement: state ? state.placement : placement,
hasPopperEscaped: state && state.modifiersData.hide ? state.modifiersData.hide.hasPopperEscaped : null,
isReferenceHidden: state && state.modifiersData.hide ? state.modifiersData.hide.isReferenceHidden : null,
arrowProps: {
style: state?.styles?.arrow,
ref: setArrowElement,
},
forceUpdate: forceUpdate || NOOP,
update: update || NOOP_PROMISE,
}),
[setPopperElement, setArrowElement, placement, state, update, forceUpdate],
);

return unwrapArray(children)(childrenProps);
}
32 changes: 32 additions & 0 deletions src/primitives/Popper/Reference/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { Ref } from "react";
import { provideRef } from "@worksolutions/react-utils";

import { unwrapArray } from "../utils";
import { ManagerReferenceNodeSetterContext } from "../Manager";

export type ReferenceChildrenProps = { ref: Ref<any> };
export type ReferenceProps = {
children: (ReferenceChildrenProps: ReferenceChildrenProps) => React.ReactNode;
innerRef?: Ref<any>;
};

export function Reference({ children, innerRef }: ReferenceProps) {
const setReferenceNode = React.useContext(ManagerReferenceNodeSetterContext);

const refHandler = React.useCallback(
(node?: HTMLElement) => {
if (!node) return;
provideRef(innerRef)(node);
setReferenceNode(node);
},
[innerRef, setReferenceNode],
);

React.useEffect(() => () => provideRef(innerRef)(null!), []);

React.useEffect(() => {
console.warn("`Reference` should not be used outside of a `Manager` component.");
}, [setReferenceNode]);

return unwrapArray(children)({ ref: refHandler });
}
58 changes: 58 additions & 0 deletions src/primitives/Popper/useVanillaPopper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useLayoutEffect, useMemo, useState } from "react";
Comment thread
grigoriy-grisha marked this conversation as resolved.
Outdated
import {
createPopper,
Instance as PopperInstance,
Modifier,
Options as PopperOptions,
Placement,
PositioningStrategy,
State as PopperState,
} from "@popperjs/core";

type Options = { createPopper?: typeof createPopper } & {
placement?: Placement;
modifiers?: Partial<Modifier<any, any>>[];
strategy?: PositioningStrategy;
onFirstUpdate?: ((state: Partial<PopperState>) => void) | undefined;
};

const EMPTY_MODIFIERS: PopperOptions["modifiers"] = [];

type UsePopperResult = {
state: PopperState | null;
update: PopperInstance["update"] | null;
forceUpdate: PopperInstance["forceUpdate"] | null;
};

export function useVanillaPopper(
reference: HTMLElement | null,
tooltip: HTMLElement | null,
options: Options,
): UsePopperResult {
const [popperInstance, setPopperInstance] = useState<PopperInstance>();

const popperOptions = useMemo(() => {
return {
onFirstUpdate: options.onFirstUpdate,
placement: options.placement || "bottom",
strategy: options.strategy || "absolute",
modifiers: options.modifiers || EMPTY_MODIFIERS,
};
}, [options.modifiers, options.placement, options.strategy, options.onFirstUpdate]);

useLayoutEffect(() => {
if (!reference) return;
if (!tooltip) return;

const popper = createPopper(reference, tooltip, popperOptions);
setPopperInstance(popper);

return () => popper.destroy();
}, [reference, tooltip, popperOptions]);

return {
forceUpdate: popperInstance ? popperInstance.forceUpdate : null,
update: popperInstance ? popperInstance.update : null,
state: popperInstance ? popperInstance.state : null,
};
}
1 change: 1 addition & 0 deletions src/primitives/Popper/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const unwrapArray = (arg: any): any => (Array.isArray(arg) ? arg[0] : arg);
Comment thread
grigoriy-grisha marked this conversation as resolved.
Outdated
4 changes: 1 addition & 3 deletions src/primitives/PopupManager/PopperElement/Arrow.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useCallback, useMemo } from "react";
import { PopperArrowProps } from "react-popper";
import { Placement } from "@popperjs/core/lib/enums";

import { PopperArrowProps } from "primitives/Popper/Popper";
import Wrapper from "../../Wrapper";

import { bottom, boxShadow, child, left, right, top, transform, zIndex } from "../../../styles";
Expand Down Expand Up @@ -40,8 +40,6 @@ interface PopupArrowInterface {
}

function Arrow({ arrowProps, placement }: PopupArrowInterface) {
return null;

const arrowPopperStyles = useCallback(() => reactStylesToStylesComponent(arrowProps.style), [arrowProps.style]);
const arrowPositionStyles = useMemo(() => getArrowPositionStyles(placement), [placement]);
const arrowStyles = useMemo(() => getArrowStyles(placement), [placement]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React, { Ref, useEffect } from "react";
import { Placement } from "@popperjs/core/lib/enums";
import { useEffectSkipFirst } from "@worksolutions/react-utils";
import { PopperArrowProps } from "react-popper";

import { PopperArrowProps } from "primitives/Popper/Popper";

import Wrapper from "../../Wrapper";
import Arrow from "./Arrow";

interface PopperChildrenProps {
styles?: any;
style: React.CSSProperties;
style: CSSStyleDeclaration;
placement: Placement;
children: React.ReactNode;
hasArrow?: boolean;
Expand All @@ -23,6 +24,7 @@ function PopperElementChildrenWrapper(
) {
useEffectSkipFirst(update, [update, hasArrow]);

useEffect(update, []);
useEffect(() => {
if (!triggerElement) return () => {};

Expand Down
44 changes: 26 additions & 18 deletions src/primitives/PopupManager/PopperElement/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import React, { Ref, useMemo } from "react";
import { Placement } from "@popperjs/core/lib/enums";
import { PositioningStrategy } from "@popperjs/core";
import popperMaxSizeModifier from "popper-max-size-modifier";
import { Modifier } from "@popperjs/core/lib";

import { Popper } from "primitives/Popper/Popper";

import { Modifier, Popper as ReactPopper } from "react-popper";
import { zIndex_popup } from "../../../constants/zIndexes";
import PopperElementChildrenWrapper from "./PopperElementChildrenWrapper";
import { popupArrowSize } from "./Arrow";
Expand All @@ -12,8 +14,12 @@ const commonPopperStyles = [zIndex_popup];

const modifierArrowPadding = 12;

function getModifiers(offset?: number): Modifier<string>[] {
function getModifiers(offset?: number): Partial<Modifier<any, any>>[] {
return [
{
name: "flip",
enabled: true,
},
{
name: "arrow",
options: {
Expand Down Expand Up @@ -70,22 +76,24 @@ function PopperElement(
const popperModifiers = useMemo(() => getModifiers(offset), [offset]);

return (
<ReactPopper placement={primaryPlacement} modifiers={popperModifiers} strategy={strategy} innerRef={ref}>
{({ ref, style, placement, arrowProps, update }) => (
<PopperElementChildrenWrapper
ref={ref}
style={style}
styles={[commonPopperStyles, styles]}
placement={placement}
arrowProps={arrowProps}
hasArrow={hasArrow}
update={update}
triggerElement={triggerElement}
>
{children}
</PopperElementChildrenWrapper>
)}
</ReactPopper>
<Popper placement={primaryPlacement} modifiers={popperModifiers} strategy={strategy} innerRef={ref}>
{({ ref, style, placement, arrowProps, update }) => {
return (
<PopperElementChildrenWrapper
ref={ref}
style={style}
styles={[commonPopperStyles, styles]}
placement={placement}
arrowProps={arrowProps}
hasArrow={hasArrow}
update={update}
triggerElement={triggerElement}
>
{children}
</PopperElementChildrenWrapper>
);
}}
</Popper>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React from "react";
import { Manager as ReactPopperManager, Reference as ReactPopperReference } from "react-popper";
import { provideRef } from "@worksolutions/react-utils";
import { observer } from "mobx-react-lite";

import { Reference } from "primitives/Popper/Reference";
import { Manager } from "primitives/Popper/Manager";

import VisibilityManager, { VisibilityManagerContextInterface } from "../../VisibilityManager";
import { SetVisibilityContextAndTriggerRef } from "./types";

Expand All @@ -27,14 +29,14 @@ function PopupManagerForClick({
const Element = React.useCallback(
(context: VisibilityManagerContextInterface) => (
<>
<ReactPopperReference>
<Reference>
{({ ref: reactPopperReferenceRef }) => (
<TriggerElement
{...context}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

мне не очень нравиться что попер так сильно связан с VisibilityManagerContextInterface. По факту, я хочу чтобы попер занимался только позиционированием моего элемента. А как и когда его показывать уже должен решать я. И желательно чтоб я это мог пропсами рулить

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

поэтому PopupManagerForClick.tsx и тот же для hover вообще не должны знать что они poper отображают

initRef={provideRef(context.initRef, reactPopperReferenceRef, setVisibilityContextAndTriggerRef(context))}
/>
)}
</ReactPopperReference>
</Reference>
</>
),
[TriggerElement, setVisibilityContextAndTriggerRef],
Expand All @@ -43,12 +45,12 @@ function PopupManagerForClick({
const ignoreElements = React.useMemo(() => [popupElementHtmlNode], [popupElementHtmlNode]);

return (
<ReactPopperManager>
<Manager>
<VisibilityManager outsideClickIgnoreElements={ignoreElements} closeOnClickOutside={closeOnClickOutside}>
{Element}
</VisibilityManager>
{popupElementNode && React.cloneElement(popupElementNode as any, { ref: setPopupElementHtmlNode })}
</ReactPopperManager>
</Manager>
);
}

Expand Down
Loading