diff --git a/package.json b/package.json index 5324f82..61d7b0b 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@types/uuid": "^8.*", "@viewstools/use-masked-input": "^2.*", "@worksolutions/ckeditor5": "0.0.3", - "@worksolutions/react-utils": "^1.2.65", + "@worksolutions/react-utils": "^1.2.67", "@worksolutions/utils": "^1.2.30", "external-svg-sprite-loader": "^6.*", "history": "^4.*", @@ -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", diff --git a/src/css/common.scss b/src/css/common.scss index 699cb9f..6c185e9 100644 --- a/src/css/common.scss +++ b/src/css/common.scss @@ -2,3 +2,4 @@ box-sizing: border-box; line-height: 0; } + diff --git a/src/primitives/Popper/Manager/index.tsx b/src/primitives/Popper/Manager/index.tsx new file mode 100644 index 0000000..1eaf178 --- /dev/null +++ b/src/primitives/Popper/Manager/index.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { useMountedState } from "react-use"; + +export const ManagerReferenceNodeContext = React.createContext(null!); +export const ManagerReferenceNodeSetterContext = React.createContext<(elem: HTMLElement) => void>(null!); + +export function Manager({ children }: React.PropsWithChildren<{}>) { + const [referenceNode, setReferenceNode] = React.useState(null!); + const isMounted = useMountedState(); + + const handleSetReferenceNode = React.useCallback((node) => isMounted() && setReferenceNode(node), [isMounted]); + + return ( + + + {children} + + + ); +} diff --git a/src/primitives/Popper/Popper/index.tsx b/src/primitives/Popper/Popper/index.tsx new file mode 100644 index 0000000..759f3b9 --- /dev/null +++ b/src/primitives/Popper/Popper/index.tsx @@ -0,0 +1,99 @@ +import React, { Ref } from "react"; +import { provideRef, useVanillaPopper } from "@worksolutions/react-utils"; +import { Modifier, Options, Placement, PositioningStrategy, State } from "@popperjs/core/lib"; + +import { ManagerReferenceNodeContext } from "../Manager"; + +import { getFirstIfArray } from "../utils"; + +type ReferenceElement = HTMLElement; + +export interface PopperArrowProps { + ref: React.Ref; + style?: Partial; +} + +export type PopperChildrenProps = { + ref: Ref; + style?: Partial; + placement: Placement; + isReferenceHidden?: boolean | null; + hasPopperEscaped?: boolean | null; + update: (() => Promise>) | null; + forceUpdate: () => void; + arrowProps: PopperArrowProps; +}; + +type Modifiers = Array>>; +export type PopperChildren = (popperChildrenProps: PopperChildrenProps) => React.ReactNode; + +export type PopperProps = { + children: PopperChildren; + innerRef?: Ref; + modifiers?: Modifiers; + placement?: Placement; + strategy?: PositioningStrategy; + referenceElement?: ReferenceElement; + onFirstUpdate?: (arg0: Partial) => void; +}; + +const noop: any = () => {}; +const noopPromise: any = () => 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: PopperChildrenProps = 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 || noopPromise, + }), + [setPopperElement, setArrowElement, placement, state, update, forceUpdate], + ); + + return getFirstIfArray(children)(childrenProps); +} diff --git a/src/primitives/Popper/Reference/index.ts b/src/primitives/Popper/Reference/index.ts new file mode 100644 index 0000000..adfd7c4 --- /dev/null +++ b/src/primitives/Popper/Reference/index.ts @@ -0,0 +1,32 @@ +import React, { Ref } from "react"; +import { provideRef } from "@worksolutions/react-utils"; + +import { getFirstIfArray } from "../utils"; +import { ManagerReferenceNodeSetterContext } from "../Manager"; + +export type ReferenceChildrenProps = { ref: Ref }; +export type ReferenceProps = { + children: (ReferenceChildrenProps: ReferenceChildrenProps) => React.ReactNode; + innerRef?: Ref; +}; + +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 getFirstIfArray(children)({ ref: refHandler }); +} diff --git a/src/primitives/Popper/utils.ts b/src/primitives/Popper/utils.ts new file mode 100644 index 0000000..3c7f628 --- /dev/null +++ b/src/primitives/Popper/utils.ts @@ -0,0 +1 @@ +export const getFirstIfArray = (arg: T | T[]): T => (Array.isArray(arg) ? arg[0] : arg); diff --git a/src/primitives/PopupManager/PopperElement/Arrow.tsx b/src/primitives/PopupManager/PopperElement/Arrow.tsx index 41bb9e4..e94afc6 100644 --- a/src/primitives/PopupManager/PopperElement/Arrow.tsx +++ b/src/primitives/PopupManager/PopperElement/Arrow.tsx @@ -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"; @@ -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]); diff --git a/src/primitives/PopupManager/PopperElement/PopperElementChildrenWrapper.tsx b/src/primitives/PopupManager/PopperElement/PopperElementChildrenWrapper.tsx index df254bb..bd5c323 100644 --- a/src/primitives/PopupManager/PopperElement/PopperElementChildrenWrapper.tsx +++ b/src/primitives/PopupManager/PopperElement/PopperElementChildrenWrapper.tsx @@ -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; @@ -23,6 +24,7 @@ function PopperElementChildrenWrapper( ) { useEffectSkipFirst(update, [update, hasArrow]); + useEffect(update, []); useEffect(() => { if (!triggerElement) return () => {}; diff --git a/src/primitives/PopupManager/PopperElement/index.tsx b/src/primitives/PopupManager/PopperElement/index.tsx index e2dbb92..ba088e2 100644 --- a/src/primitives/PopupManager/PopperElement/index.tsx +++ b/src/primitives/PopupManager/PopperElement/index.tsx @@ -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"; @@ -12,8 +14,12 @@ const commonPopperStyles = [zIndex_popup]; const modifierArrowPadding = 12; -function getModifiers(offset?: number): Modifier[] { +function getModifiers(offset?: number): Partial>[] { return [ + { + name: "flip", + enabled: true, + }, { name: "arrow", options: { @@ -70,22 +76,24 @@ function PopperElement( const popperModifiers = useMemo(() => getModifiers(offset), [offset]); return ( - - {({ ref, style, placement, arrowProps, update }) => ( - - {children} - - )} - + + {({ ref, style, placement, arrowProps, update }) => { + return ( + + {children} + + ); + }} + ); } diff --git a/src/primitives/PopupManager/PopupManagers/PopupManagerForClick.tsx b/src/primitives/PopupManager/PopupManagers/PopupManagerForClick.tsx index b24ca82..80aaf0f 100644 --- a/src/primitives/PopupManager/PopupManagers/PopupManagerForClick.tsx +++ b/src/primitives/PopupManager/PopupManagers/PopupManagerForClick.tsx @@ -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"; @@ -27,14 +29,14 @@ function PopupManagerForClick({ const Element = React.useCallback( (context: VisibilityManagerContextInterface) => ( <> - + {({ ref: reactPopperReferenceRef }) => ( )} - + ), [TriggerElement, setVisibilityContextAndTriggerRef], @@ -43,12 +45,12 @@ function PopupManagerForClick({ const ignoreElements = React.useMemo(() => [popupElementHtmlNode], [popupElementHtmlNode]); return ( - + {Element} {popupElementNode && React.cloneElement(popupElementNode as any, { ref: setPopupElementHtmlNode })} - + ); } diff --git a/src/primitives/PopupManager/PopupManagers/PopupManagerForExternalControl.tsx b/src/primitives/PopupManager/PopupManagers/PopupManagerForExternalControl.tsx index 877a77d..ef2d237 100644 --- a/src/primitives/PopupManager/PopupManagers/PopupManagerForExternalControl.tsx +++ b/src/primitives/PopupManager/PopupManagers/PopupManagerForExternalControl.tsx @@ -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 { Manager } from "primitives/Popper/Manager"; +import { Reference } from "primitives/Popper/Reference"; + import VisibilityManager, { VisibilityManagerContextInterface } from "../../VisibilityManager"; import { SetVisibilityContextAndTriggerRef } from "./types"; @@ -22,7 +24,7 @@ function PopupManagerForExternalControl({ const Element = React.useCallback( (context: VisibilityManagerContextInterface) => ( <> - + {({ ref: reactPopperReferenceRef }) => ( )} - + ), [TriggerElement, setVisibilityContextAndTriggerRef], @@ -40,10 +42,10 @@ function PopupManagerForExternalControl({ return ( <> - + {Element} {popupElementNode} - + ); } diff --git a/src/primitives/PopupManager/PopupManagers/PopupManagerForHover.tsx b/src/primitives/PopupManager/PopupManagers/PopupManagerForHover.tsx index ed4dff5..7049938 100644 --- a/src/primitives/PopupManager/PopupManagers/PopupManagerForHover.tsx +++ b/src/primitives/PopupManager/PopupManagers/PopupManagerForHover.tsx @@ -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"; @@ -26,14 +28,14 @@ function PopupManagerForHover({ const Element = React.useCallback( (context: VisibilityManagerContextInterface) => ( <> - + {({ ref: reactPopperReferenceRef }) => ( )} - + ), [TriggerElement, setVisibilityContextAndTriggerRef], @@ -41,10 +43,10 @@ function PopupManagerForHover({ return ( <> - + {Element} {popupElementNode} - + ); }