@popperjs/core#createPopper TypeScript Examples

The following examples show how to use @popperjs/core#createPopper. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: popper.directive.ts    From TypeFast with MIT License 6 votes vote down vote up
ngOnInit(): void {
    let tooltipEl = this.el.nativeElement.getElementsByClassName(
      'tooltip'
    )[0] as HTMLElement;

    if (this.text) {
      tooltipEl = document.createElement('div');
      tooltipEl.className = 'tooltip';
      tooltipEl.innerText = this.text;
      document.body.appendChild(tooltipEl);
    }
    this.popper = createPopper(this.el.nativeElement, tooltipEl, {
      placement: 'right' as Placement,
    });
  }
Example #2
Source File: suggest.ts    From Templater with GNU Affero General Public License v3.0 6 votes vote down vote up
open(container: HTMLElement, inputEl: HTMLElement): void {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        (<any>this.app).keymap.pushScope(this.scope);

        container.appendChild(this.suggestEl);
        this.popper = createPopper(inputEl, this.suggestEl, {
            placement: "bottom-start",
            modifiers: [
                {
                    name: "sameWidth",
                    enabled: true,
                    fn: ({ state, instance }) => {
                        // Note: positioning needs to be calculated twice -
                        // first pass - positioning it according to the width of the popper
                        // second pass - position it with the width bound to the reference element
                        // we need to early exit to avoid an infinite loop
                        const targetWidth = `${state.rects.reference.width}px`;
                        if (state.styles.popper.width === targetWidth) {
                            return;
                        }
                        state.styles.popper.width = targetWidth;
                        instance.update();
                    },
                    phase: "beforeWrite",
                    requires: ["computeStyles"],
                },
            ],
        });
    }
Example #3
Source File: DatePicker.tsx    From symphony-ui-toolkit with Apache License 2.0 6 votes vote down vote up
mountDayPickerInstance() {
    const { placement, menuShouldBlockScroll } = this.props;
    const { popperElement, referenceElement, refContainer } = this.state;
    this.dayPickerInstance = createPopper(referenceElement, popperElement, {
      placement: `${placement}-start` as
        | 'bottom-start'
        | 'top-start'
        | 'right-start'
        | 'left-start',
      modifiers: [
        {
          name: 'flip',
          options: {
            fallbackPlacements: ['top-start', 'right-start', 'left-start'],
          },
        },
        {
          name: 'offset',
          options: {
            offset: [0, 4],
          },
        },
      ],
    });


    if (menuShouldBlockScroll) {
      const scrollContainer = getScrollParent(refContainer);
      if (scrollContainer) {
        const wheelEvent = EventListener.onwheel in document.createElement('div') ? EventListener.wheel : EventListener.mousewheel;

        scrollContainer.addEventListener(EventListener.DOMMouseScroll, this.handleScrollParent); // older Firefox
        scrollContainer.addEventListener(EventListener.touchmove, this.handleScrollParent); // mobile
        scrollContainer.addEventListener(wheelEvent, this.handleScrollParent); // modern desktop
        scrollContainer.addEventListener(EventListener.keydown, this.handleKeydownScrollParent);
      }
    }
  }
Example #4
Source File: suggest.ts    From quickadd with MIT License 6 votes vote down vote up
open(container: HTMLElement, inputEl: HTMLElement): void {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        (<any>this.app).keymap.pushScope(this.scope);

        container.appendChild(this.suggestEl);
        this.popper = createPopper(inputEl, this.suggestEl, {
            placement: "bottom-start",
            modifiers: [
                {
                    name: "sameWidth",
                    enabled: true,
                    fn: ({ state, instance }) => {
                        // Note: positioning needs to be calculated twice -
                        // first pass - positioning it according to the width of the popper
                        // second pass - position it with the width bound to the reference element
                        // we need to early exit to avoid an infinite loop
                        const targetWidth = `${state.rects.reference.width}px`;
                        if (state.styles.popper.width === targetWidth) {
                            return;
                        }
                        state.styles.popper.width = targetWidth;
                        instance.update();
                    },
                    phase: "beforeWrite",
                    requires: ["computeStyles"],
                },
            ],
        });
    }
Example #5
Source File: suggester.ts    From obsidian-initiative-tracker with GNU General Public License v3.0 6 votes vote down vote up
open(): void {
        // TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        (<any>this.app).keymap.pushScope(this.scope);

        document.body.appendChild(this.suggestEl);
        this.popper = createPopper(this.inputEl, this.suggestEl, {
            placement: "auto-start",
            modifiers: [
                {
                    name: "offset",
                    options: {
                        offset: [0, 10]
                    }
                },
                {
                    name: "flip",
                    options: {
                        allowedAutoPlacements: ["top-start", "bottom-start"]
                    }
                }
            ]
        });
    }
Example #6
Source File: suggester.ts    From obsidian-fantasy-calendar with MIT License 6 votes vote down vote up
open(): void {
        // TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
        this.app.keymap.pushScope(this.scope);

        document.body.appendChild(this.suggestEl);
        this.popper = createPopper(this.inputEl, this.suggestEl, {
            placement: "bottom-start",
            modifiers: [
                {
                    name: "offset",
                    options: {
                        offset: [0, 10]
                    }
                },
                {
                    name: "flip",
                    options: {
                        fallbackPlacements: ["top"]
                    }
                }
            ]
        });
    }
Example #7
Source File: index.ts    From obsidian-admonition with MIT License 6 votes vote down vote up
open(): void {
        // TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        (<any>this.app).keymap.pushScope(this.scope);

        document.body.appendChild(this.suggestEl);
        this.popper = createPopper(this.inputEl, this.suggestEl, {
            placement: "bottom-start",
            modifiers: [
                {
                    name: "offset",
                    options: {
                        offset: [0, 10]
                    }
                },
                {
                    name: "flip",
                    options: {
                        fallbackPlacements: ["top"]
                    }
                }
            ]
        });
    }
Example #8
Source File: useOverlayPosition.ts    From kodiak-ui with MIT License 6 votes vote down vote up
export function useOverlayPosition(
  {
    isVisible,
    placement = 'bottom-start',
    offset = [0, 8],
  }: UseOverlayPositionProps,
  triggerRef,
  overlayRef,
) {
  useLayoutEffect(() => {
    if (!isVisible || !triggerRef || !overlayRef) {
      return
    }

    const popperInstance = createPopper(
      triggerRef?.current,
      overlayRef?.current,
      {
        placement,
        modifiers: [{ name: 'offset', options: { offset } }],
      },
    )

    return () => {
      popperInstance.destroy()
    }
  }, [isVisible, offset, overlayRef, placement, triggerRef])

  return null
}
Example #9
Source File: tooltip.ts    From rabbit-ui with MIT License 6 votes vote down vote up
export function _newCreatePopper(
    reference: Element,
    popper: HTMLElement,
    placement: string | any,
    offset: number
): any {
    return createPopper(reference, popper, {
        placement: placement, // 设置位置
        modifiers: [
            {
                name: 'computeStyles',
                options: {
                    gpuAcceleration: false // 使用top/left属性。否则会和弹出器动画冲突
                }
            },
            {
                name: 'computeStyles',
                options: {
                    adaptive: false // 避免重新计算弹出器位置从而造成位置牛头不对马嘴
                }
            },
            {
                name: 'offset',
                options: {
                    offset: [offset] // 自定义弹出器出现位置的偏移量
                }
            }
        ]
    });
}
Example #10
Source File: livePreview.tsx    From roam-toolkit with MIT License 6 votes vote down vote up
private makePopper(target: HTMLElement) {
        this.popper = createPopper(target, this.iframe, {
            placement: 'right',
            modifiers: [
                {
                    name: 'preventOverflow',
                    options: {
                        padding: {top: 48},
                    },
                },
                {
                    name: 'flip',
                    options: {
                        boundary: document.querySelector('#app'),
                    },
                },
            ],
        })
    }
Example #11
Source File: Popup.tsx    From aria-devtools with MIT License 5 votes vote down vote up
Popup = ({
  onMouseOver,
  onMouseLeave,
  children,
  targetElementRef
}) => {
  const popupContainer = React.useContext(PopupContext);
  const popupWrapper = React.useRef(null);

  React.useEffect(() => {
    const instance = createPopper(
      targetElementRef.current,
      popupWrapper.current,
      {
        placement: "auto",
        modifiers: [
          {
            name: "arrow",
            options: {
              padding: 30
            }
          }
        ]
      }
    );
    return () => {
      instance.destroy();
    };
  }, [targetElementRef.current, popupContainer.current]);

  if (!popupContainer.current) return null;
  return ReactDOM.createPortal(
    <PopupDiv
      onMouseOver={onMouseOver}
      onMouseLeave={onMouseLeave}
      ref={popupWrapper}
    >
      <div data-popper-arrow={true} />
      <div data-popper-content={true}>{children}</div>
    </PopupDiv>,
    popupContainer.current
  );
}
Example #12
Source File: dropdown.directive.ts    From sba-angular with MIT License 5 votes vote down vote up
show(usePopper = false) {
        if (!this.dropdownToggle || !this.dropdownMenu || !this.dropdown) {
            return;
        }
        if (/*TODO element.disabled || */
            this.dropdownToggle.classList.contains(gClassName.DISABLED) ||
            this.dropdownMenu.classList.contains(gClassName.SHOW)) {
          return;
        }

        const parent = this.dropdown;

        // Disable totally Popper.js for Dropdown in Navbar
        if (!this.inNavbar || usePopper) {
            let referenceElement = this.dropdownToggle;

            if (gConfig.reference === 'parent') {
                referenceElement = parent;
            }

            // If boundary is not `scrollParent`, then set position to `static`
            // to allow the menu to "escape" the scroll parent's boundaries
            // https://github.com/twbs/bootstrap/issues/24251
            if (gConfig.boundary !== 'clippingParents') {
                parent.classList.add(gClassName.POSITION_STATIC);
            }
            this.popper = createPopper(referenceElement, this.dropdownMenu, this.getPopperConfig());
        }

        // If this is a touch-enabled device we add extra
        // empty mouseover listeners to the body's immediate children;
        // only needed because of broken event delegation on iOS
        // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
        if ('ontouchstart' in document.documentElement &&
            !parent.closest(gSelector.NAVBAR_NAV)) {
            Array.from(document.body.children).forEach((element) => element.addEventListener('mouseover', noop));
        }

        this.dropdownToggle.focus();
        this.dropdownToggle.setAttribute('aria-expanded', "true");

        this.dropdownMenu.classList.toggle(gClassName.SHOW);
        parent.classList.toggle(gClassName.SHOW);
    }
Example #13
Source File: insertionLine.ts    From starboard-notebook with Mozilla Public License 2.0 5 votes vote down vote up
firstUpdated() {
    this.insertPosition = this.classList.contains("insertion-line-top") ? "before" : "after";
    if (!globalCellTypePicker) {
      // TODO: Flow runtime into this some nicer way.
      globalCellTypePicker = new CellTypePicker((window as any).runtime);
    }
    this.classList.add("line-grid");
    let unpop: () => void;
    let lastActive: number;
    let popoverIsActive = false;
    // TODO: refactor into separate function (and maybe find a way to detect "out of bounds" click in a nicer way)
    if (this.buttonElement !== undefined) {
      const btn = this.buttonElement;
      this.buttonElement.addEventListener("click", (_: MouseEvent) => {
        if (popoverIsActive) return;
        this.appendChild(globalCellTypePicker);
        lastActive = Date.now();
        const listener = (evt: MouseEvent) => {
          const isClickInside = globalCellTypePicker.contains(evt.target as any);
          if (!isClickInside) {
            unpop();
          }
        };
        unpop = () => {
          // Clean up the overlay
          if (Date.now() - lastActive < 100) {
            return;
          }
          popoverIsActive = false;
          pop.destroy();
          globalCellTypePicker.remove();
          document.removeEventListener("click", listener);
        };
        document.addEventListener("click", listener);
        const pop = createPopper(btn, globalCellTypePicker, {
          placement: "right-start",
          strategy: "fixed",
        });
        const parent = this.parentElement;
        if (parent && parent instanceof CellElement) {
          globalCellTypePicker.setHighlightedCellType(parent.cell.cellType);
        }
        globalCellTypePicker.onInsert = (cellData: Partial<Cell>) => {
          // Right now we assume the insertion line has a cell as parent
          if (parent && parent instanceof CellElement) {
            this.runtime.controls.insertCell({
              adjacentCellId: parent.cell.id,
              position: this.insertPosition,
              data: cellData,
            });
            unpop();
          }
        };
        popoverIsActive = true;
      });
    }
  }
Example #14
Source File: tooltip.tsx    From nota with MIT License 4 votes vote down vote up
Tooltip = observer(({ children: Inner, Popup }: TooltipProps) => {
  const [referenceElement, setReferenceElement] = useState<HTMLElement | null>(null);
  const [popperElement, setPopperElement] = useState<HTMLElement | null>(null);
  const [arrowElement, setArrowElement] = useState<HTMLElement | null>(null);
  const [instance, setInstance] = useState<Instance | null>(null);

  let ctx = usePlugin(TooltipPlugin);
  let [id] = useState(_.uniqueId());
  let [stage, setStage] = useState("start");
  let [show, setShow] = useState(false);

  let trigger: React.MouseEventHandler = e => {
    e.preventDefault();

    if (show) {
      setShow(false);
    } else {
      if (stage == "start") {
        setStage("mount");
      }
      ctx.queueUpdate(id);
    }
  };

  useEffect(() => {
    if (stage == "mount" && referenceElement && popperElement) {
      setStage("done");

      // TODO: I noticed that when embedding a document within another,
      //   the nested-document tooltips on math elements would be misaligned.
      //   Unclear why, but a fix was to use the popperjs "virtual element"
      //   feature that manually calls getBoundingClientRect(). Probably an issue
      //   with whatever their getBoundingClientRect alternative is?
      let popperRefEl = {
        getBoundingClientRect: () => referenceElement.getBoundingClientRect(),
      };

      let instance = createPopper(popperRefEl, popperElement, {
        placement: "top",
        modifiers: [
          // Push tooltip farther away from content
          { name: "offset", options: { offset: [0, 10] } },

          // Add arrow
          { name: "arrow", options: { element: arrowElement } },
        ],
      });
      setInstance(instance);

      ctx.elts[id] = {
        popperElement,
        referenceElement: popperRefEl,
        instance,
        setShow,
      };
      ctx.checkQueue();
    }
  }, [stage, referenceElement, popperElement]);

  let inner = isConstructor(Inner) ? (
    <Inner ref={setReferenceElement} onClick={trigger} />
  ) : (
    <Container ref={setReferenceElement} onClick={trigger}>
      {Inner}
    </Container>
  );

  return (
    <>
      {inner}
      {stage != "start" ? (
        <ToplevelElem>
          <div
            className="tooltip"
            ref={setPopperElement}
            style={
              {
                ...(stage == "done" ? instance!.state.styles.popper : {}),
                // Have to use visibility instead of display so tooltips can
                // correctly compute position for stacking
                visibility: show ? "visible" : "hidden",
              } as any
            }
            {...(stage == "done" ? instance!.state.attributes.popper : {})}
          >
            <div
              className="arrow"
              ref={setArrowElement}
              style={{
                // Can't use visibility here b/c it messes with the special CSS for arrows
                display: show ? "block" : "none",
              }}
            />
            {getOrRender(Popup, {})}
          </div>
        </ToplevelElem>
      ) : null}
    </>
  );
})
Example #15
Source File: useMenu.ts    From kodiak-ui with MIT License 4 votes vote down vote up
export function useMenu({
  placement = 'bottom-start',
  offset = [0, 8],
}: UseMenuProps = {}): UseMenuReturnValue {
  const buttonRef = React.useRef<HTMLButtonElement | null>(null)
  const menuRef = React.useRef<HTMLUListElement>()
  const itemsRef = React.useRef<{ [key: string]: HTMLLIElement | Element }>({})
  const itemHandlersRef = React.useRef<{
    [key: string]: ((event: any) => void) | undefined
  }>({})
  const elementIds = React.useRef<ElementIds>(generateElementIds())
  const popperInstanceRef = React.useRef<any | null>(null)

  const [activeItem, setActiveItem] = React.useState('')

  const {
    isOpen: isExpanded,
    handleOpenPortal,
    handleClosePortal,
    Portal,
  } = usePortal()

  useIsomorphicLayoutEffect(
    function initializePopper() {
      if (!isExpanded && (!buttonRef.current || !menuRef.current)) {
        return
      }

      const popperInstance = createPopper(
        buttonRef.current as Element | VirtualElement,
        menuRef.current as HTMLElement,
        {
          placement,
          modifiers: [{ name: 'offset', options: { offset } }],
        },
      )

      popperInstanceRef.current = popperInstance

      return () => {
        popperInstance.destroy()
        popperInstanceRef.current = null
      }
    },
    [isExpanded, placement, offset],
  )

  function getItemNodeFromIndex(index: number): HTMLLIElement | Element | null {
    return itemsRef && itemsRef.current && itemsRef.current[index]
  }

  function focusMenuRef() {
    menuRef && menuRef.current && menuRef.current.focus()
  }

  function focusButtonRef() {
    buttonRef && buttonRef.current && buttonRef.current.focus()
  }

  useKey({
    key: 'Escape',
    target: menuRef.current,
    handler: () => {
      if (isExpanded) {
        handleClosePortal({})
      }
      focusButtonRef()
    },
  })

  useKey({
    key: 'ArrowDown',
    target: menuRef.current,
    handler: () => {
      const nextItem = getNextItem({
        moveAmount: 1,
        baseIndex: Object.keys(itemsRef.current).indexOf(activeItem),
        items: itemsRef.current,
        getItemNodeFromIndex,
      })

      setActiveItem(nextItem)

      setAttributes(itemsRef.current[nextItem], {
        'aria-selected': 'true',
      })
    },
  })

  useKey({
    key: 'ArrowUp',
    target: menuRef.current,
    handler: () => {
      const nextItem = getNextItem({
        moveAmount: -1,
        baseIndex: Object.keys(itemsRef.current).indexOf(activeItem),
        items: itemsRef.current,
        getItemNodeFromIndex,
      })

      setActiveItem(nextItem)

      setAttributes(itemsRef.current[nextItem], {
        'aria-selected': 'true',
      })
    },
  })

  useKey({
    key: 'Enter',
    target: menuRef.current,
    handler: () => {
      const handler = itemHandlersRef.current[activeItem] as () => void
      handler?.()
    },
  })

  useOnClickOutside({
    ref: menuRef as React.MutableRefObject<Element>,
    refException: buttonRef as React.MutableRefObject<Element>,
    handler: () => {
      handleClosePortal({})
    },
  })

  React.useEffect(() => {
    if (isExpanded) {
      focusMenuRef()
    }
  }, [isExpanded])

  function registerButtonElement({
    ref,
    options,
  }: RefAndOptions<Element>): RefAndOptions<Element> {
    if (ref && ref.tagName === MenuElementTagNames.Button) {
      buttonRef.current = ref as HTMLButtonElement

      setAttributes(buttonRef.current, {
        id: elementIds.current.buttonId,
        'aria-haspopup': 'true',
        'aria-controls': 'IDREF',
      })
    }

    return { ref, options }
  }

  function registerMenuElement({
    ref,
    options,
  }: RefAndOptions<Element>): RefAndOptions<Element> {
    if (ref && ref.tagName === MenuElementTagNames.Ul) {
      menuRef.current = ref as HTMLUListElement

      setAttributes(menuRef.current, {
        id: elementIds.current.menuId,
        role: 'menu',
        tabIndex: '-1',
        'aria-labelledby':
          buttonRef && buttonRef.current ? `${buttonRef.current.id}` : '',
      })
    }

    return { ref, options }
  }

  function registerMenuItemElement({
    ref,
    options,
  }: RefAndOptions<Element>): RefAndOptions<Element> {
    if (ref && ref.tagName === MenuElementTagNames.Li) {
      const name = options && options.name
      const handler = options && options.handler
      const items = itemsRef.current
      const itemHandlers = itemHandlersRef.current

      items[name as string] = ref
      itemHandlers[name as string] = handler

      const item = items[name as string]

      setAttributes(item, {
        id: elementIds.current.getItemId(name as string),
        role: 'option',
        'aria-selected': 'false',
      })
    }

    return { ref, options }
  }

  const registerElementRefs = React.useCallback(function registerElementRefs(
    ref: Element | null,
    options?: RegisterOptions,
  ): RefAndOptions<Element> {
    return registerMenuItemElement(
      registerMenuElement(registerButtonElement({ ref, options })),
    )
  },
  [])

  /**
   * Register the menu elements
   *
   * Allows the ability to add the appropriate HTML attributes
   * to an HTML element.
   */
  const register = React.useCallback(
    function register(
      ref: (HTMLButtonElement | HTMLUListElement | HTMLLIElement) | null,
      options?: RegisterOptions,
    ): RefAndOptions<Element> | null {
      return ref && registerElementRefs(ref, options)
    },
    [registerElementRefs],
  )

  const handleToggleMenu = React.useCallback(
    function handleToggleMenu(event) {
      setAttributes(buttonRef && (buttonRef.current as Element | null), {
        'aria-expanded': `${!isExpanded}`,
      })

      isExpanded ? handleClosePortal(event) : handleOpenPortal(event)
    },
    [isExpanded, handleOpenPortal, handleClosePortal],
  )

  const handleCloseMenu = React.useCallback(
    function handleCloseMenu() {
      setAttributes(buttonRef && (buttonRef.current as Element | null), {
        'aria-expanded': 'false',
      })

      handleClosePortal({})
    },
    [handleClosePortal],
  )

  function getItemProps(name: string) {
    return {
      onClick: (event: React.MouseEvent<any, MouseEvent>) => {
        // TS doesn't like this itemHandlersRef.current[name] && itemHandlersRef.current[name](event)
        // this will work when the bundler supports conditional chaining itemHandlersRef.current[name]?.(event)
        const itemHandler = itemHandlersRef.current[name]
        itemHandler && itemHandler(event)
      },
      onMouseEnter: () => setActiveItem(name),
    }
  }

  return {
    register,
    isExpanded,
    activeItem,
    handleToggleMenu,
    handleCloseMenu,
    getItemProps,
    Menu: Portal,
  }
}
Example #16
Source File: useTooltip.ts    From kodiak-ui with MIT License 4 votes vote down vote up
export function useTooltip({
  placement = 'top',
  offset = [0, 10],
  closeTimeout = 0,
}: UseTooltipProps = {}): UseTooltipReturn {
  const triggerRef = React.useRef<HTMLElement | null>(null)
  const tooltipRef = React.useRef<HTMLElement | null>(null)
  const arrowRef = React.useRef<HTMLElement | null>(null)
  const popperInstanceRef = React.useRef<any>(null)
  const timeoutIdRef = React.useRef<any>(null)

  const id = useId()

  const {
    isOpen: isVisible,
    handleOpenPortal,
    handleClosePortal: instantlyClosePortal,
    Portal,
    portalRef,
  } = usePortal()

  const delayedClosePortal = React.useCallback(
    function delayedClosePortal(event) {
      clearTimeout(timeoutIdRef.current)
      if (closeTimeout === 0) {
        instantlyClosePortal(event)
      } else {
        timeoutIdRef.current = setTimeout(() => {
          return instantlyClosePortal(event)
        }, closeTimeout)
      }
    },
    [instantlyClosePortal, closeTimeout],
  )

  React.useLayoutEffect(
    function initializePopper() {
      if (!isVisible && (!triggerRef.current || !tooltipRef.current)) {
        return
      }

      const popperInstance = createPopper(
        triggerRef.current as Element | VirtualElement,
        tooltipRef.current as HTMLElement,
        {
          placement,
          modifiers: [
            offset ? { name: 'offset', options: { offset } } : {},
            arrowRef && arrowRef.current
              ? {
                  name: 'arrow',
                  options: { element: arrowRef && arrowRef.current },
                }
              : {},
          ],
        },
      )

      popperInstanceRef.current = popperInstance

      return () => {
        popperInstance.destroy()
        popperInstanceRef.current = null
      }
    },
    [isVisible, offset, placement],
  )

  useOnClickOutside({
    ref: portalRef as React.MutableRefObject<Element>,
    refException: triggerRef as React.MutableRefObject<Element>,
    handler: () => {
      instantlyClosePortal({})
    },
  })

  useKey({
    key: 'Escape',
    target: triggerRef.current,
    handler: () => {
      if (isVisible) {
        instantlyClosePortal({})
      }
    },
  })

  function registerTriggerElement({
    ref,
    options,
  }: RefAndOptions): RefAndOptions {
    if (ref && options && options.trigger) {
      triggerRef.current = ref

      setAttributes(triggerRef.current, {
        'aria-describedby': `kodiak-ui-tooltip-${id}`,
      })
    }

    return { ref, options }
  }

  function registerTooltipElement({
    ref,
    options,
  }: RefAndOptions): RefAndOptions {
    if ((ref && !options) || (options && !options.trigger && !options.arrow)) {
      tooltipRef.current = ref
      setAttributes(tooltipRef.current, {
        id: `kodiak-ui-tooltip-${id}`,
        role: 'tooltip',
      })
    }

    return { ref, options }
  }

  function registerArrowElement({
    ref,
    options,
  }: RefAndOptions): RefAndOptions {
    if (ref && options && options.arrow) {
      arrowRef.current = ref
    }

    return { ref, options }
  }

  function register(
    ref: TooltipRef,
    options?: RegisterOptions,
  ): {
    ref: TooltipRef
    options?: RegisterOptions
  } {
    return registerArrowElement(
      registerTriggerElement(registerTooltipElement({ ref, options })),
    )
  }

  const getTriggerProps = React.useCallback(
    function getTriggerProps() {
      return {
        onFocus: handleOpenPortal,
        onBlur: delayedClosePortal,
        onMouseEnter: handleOpenPortal,
        onMouseLeave: delayedClosePortal,
      }
    },
    [handleOpenPortal, delayedClosePortal],
  )

  return {
    isVisible,
    register,
    getTriggerProps,
    Portal,
  }
}
Example #17
Source File: popper.ts    From ExpressiveAnimator with Apache License 2.0 4 votes vote down vote up
export function popperAction(trigger: HTMLElement, {content, options, closeOnClickOutside, update}) {
    let instance = null;
    let open: boolean = false;

    const clickOutside = e => {
        if (instance && open && !trigger.contains(e.target)) {
            hide();
        }
    };

    const toggle = () => {
        if (!open) {
            show();
        } else {
            hide();
        }
    }

    const show = () => {
        if (instance) {
            instance.setOptions(options);
            instance.update();
        } else {
            instance = createPopper(trigger, content(), options);
        }
        if (closeOnClickOutside) {
            outsideEventHandler(clickOutside, true);
        }
        open = true;
        update(true);
    }

    const hide = () => {
        if (instance) {
            instance.setOptions(hideOptions);
        }
        if (closeOnClickOutside) {
            outsideEventHandler(clickOutside, false);
        }
        open = false;
        update(false);
    }

    trigger.addEventListener('popper-show', show);
    trigger.addEventListener('popper-hide', hide);
    trigger.addEventListener('popper-toggle', toggle);

    return {
        update(params) {
            if (!params) {
                return;
            }

            let force = false;

            if (params?.options) {
                options = params.options;
                force = true;
            }
            if (params?.closeOnClickOutside !== closeOnClickOutside) {
                closeOnClickOutside = !!params?.closeOnClickOutside;
                outsideEventHandler(clickOutside, closeOnClickOutside);
            }
            if (params?.update) {
                update = params?.update;
            }

            if (params?.content !== content) {
                content = params.content;
                if (instance) {
                    instance.destroy();
                    if (open) {
                        instance = createPopper(trigger, content(), options);
                        force = false;
                    }
                }
            }

            if (force && open && instance) {
                instance.setOptions(options);
                instance.update();
            }
        },
        destroy() {
            if (instance) {
                instance.destroy();
                instance = null;
            }
            trigger.removeEventListener('popper-show', show);
            trigger.removeEventListener('popper-hide', hide);
            trigger.removeEventListener('popper-toggle', toggle);
            if (closeOnClickOutside) {
                outsideEventHandler(clickOutside, false);
            }
        }
    };
}
Example #18
Source File: Root.tsx    From houston with MIT License 4 votes vote down vote up
PopoverRoot: React.FC<IPopoverProps> = ({ children }) => {
  const [state, setState] = React.useState<IState>({
    opened: false,
    target: null,
    content: null,
    closedTarget: null,
    timestamp: 0,
    placement: 'auto'
  });

  React.useEffect(() => {
    if (!state.opened) {
      state.content?.classList?.remove('--opened');
      return undefined;
    }

    const instance = createPopper(state.target, state.content, {
      placement: state.placement ?? 'auto',
      modifiers: [
        {
          name: 'offset',
          options: {
            offset: [0, 8]
          }
        }
      ]
    });
    state.content?.classList?.add('--opened');

    return () => {
      state.content?.classList?.remove('--opened');
      setTimeout(() => instance.destroy(), 100);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [state.opened]);

  useOnClickOutside(
    state.content,
    () => {
      const justOpened = (Date.now() - state.timestamp ?? 0) < 100;
      if (!state.opened || justOpened) return;

      setState(currentState => ({ ...currentState, opened: false, closedTarget: currentState.target }));
      setTimeout(() => {
        setState(currentState => ({ ...currentState, closedTarget: null }));
      }, 300);
    },
    [state.opened]
  );

  const contextSetValue = React.useCallback<IPopoverContext['setState']>(newState => {
    setTimeout(() => {
      const resolveNewState = (currentState: IState) => ({
        ...currentState,
        ...newState,
        timestamp: Date.now(),
        opened: currentState.closedTarget === newState.target ? false : true
      });

      setState(currentState => {
        const waitPreviousclose = !!currentState.closedTarget;
        if (waitPreviousclose) {
          setTimeout(() => setState(resolveNewState), 100);
          return currentState;
        }

        return resolveNewState(currentState);
      });
    }, 0);

    return () => {
      setState(currentState => {
        if (currentState.target !== newState.target) return currentState;
        return { ...currentState, opened: false };
      });
    };
  }, []);

  const contextValue = React.useMemo<IPopoverContext>(
    () => ({ setState: contextSetValue, openedTarget: state.opened ? state.target : null }),
    [contextSetValue, state.opened, state.target]
  );

  return <PopoverContext.Provider value={contextValue}>{children}</PopoverContext.Provider>;
}
Example #19
Source File: index.ts    From elenext with MIT License 4 votes vote down vote up
usePopper = (props: UsePopperOptions) => {
  const popperId = uniqueId('el-popper')
  const { referenceRef, popperRef } = props
  const timers: {
    showTimer: any
    hideTimer: any
  } = { showTimer: undefined, hideTimer: undefined }

  const state = reactive<usePopperState>({
    instance: null,
    popperId,
    attrs: {
      styles: {
        popper: {
          position: 'absolute',
          left: '0',
          top: '0',
        },
        arrow: {
          position: 'absolute',
        },
      },
      attributes: {},
    },
  })

  const popperOptions = computed<PopperOptions>(() => {
    return {
      placement: props.placement || 'bottom-start',
      strategy: 'absolute',
      modifiers: [
        {
          name: 'updateState',
          enabled: true,
          phase: 'write',
          fn: ({ state: popperState }: any) => {
            const elements = Object.keys(popperState.elements)
            state.attrs = {
              styles: fromEntries(elements.map(element => [element, popperState.styles[element] || {}])),
              attributes: fromEntries(elements.map(element => [element, popperState.attributes[element] || {}])),
            }
          },
          requires: ['computeStyles'],
        },
        { name: 'applyStyles', enabled: false },
        { name: 'offset', options: { offset: [0, props.offset || 0] } },
      ],
    }
  })

  const clearScheduled = () => {
    clearTimeout(timers.hideTimer)
    clearTimeout(timers.showTimer)
  }

  let clickEvent: any = null

  const togglePopper = (event: MouseEvent) => {
    clickEvent = event
    props.onTrigger(popperId)
  }
  const showPopper = () => {
    clearScheduled()
    timers.showTimer = setTimeout(() => {
      props.onTrigger(popperId, true)
    }, 0)
  }
  const hidePopper = () => {
    clearScheduled()
    timers.hideTimer = setTimeout(() => {
      props.onTrigger(popperId, false)
    }, props.hideDaly || 200)
  }
  const outSideClickHandler = (event: MouseEvent) => {
    // outSideClick 和 togglePopper 冲突
    if (event === clickEvent) {
      return
    }
    if (popperRef.value && !popperRef.value.contains(event.target as Node)) {
      if (
        ['hover', 'focus'].indexOf(props.trigger) !== -1 &&
        referenceRef.value &&
        referenceRef.value.contains(event.target as Node)
      ) {
        return
      } else {
        hidePopper()
      }
    }
  }

  const eventRegOrUnReg = isReg => {
    const referenceEl = referenceRef.value
    const popperEl = popperRef.value
    const event = isReg ? 'addEventListener' : 'removeEventListener'
    if (referenceEl && popperEl) {
      if (props.trigger === 'hover') {
        referenceEl[event]('mouseenter', showPopper)
        referenceEl[event]('mouseleave', hidePopper)
        popperEl[event]('mouseenter', showPopper)
        popperEl[event]('mouseleave', hidePopper)
      }
      if (props.trigger === 'click') {
        referenceEl[event]('click', togglePopper)
        // popperEl[event]('mouseenter', showPopper)
        // popperEl[event]('mouseleave', hidePopper)
      }
      if (props.trigger === 'focus') {
        referenceEl[event]('focus', showPopper)
        referenceEl[event]('blur', hidePopper)
      }
      if (props.trigger !== 'manual') {
        document[event]('click', outSideClickHandler)
      }
    }
  }

  watchEffect(() => {
    if (state.instance) {
      state.instance.setOptions(popperOptions.value)
    }
  })

  watch([referenceRef, popperRef], () => {
    const referenceEl = referenceRef.value
    const popperEl = popperRef.value
    if (referenceEl && popperEl) {
      if (state.instance) {
        state.instance.destroy()
      }
      state.instance = createPopper(referenceEl, popperEl as HTMLElement, popperOptions.value)
    }
  })

  watchEffect(onInvalidate => {
    const referenceEl = referenceRef.value
    const popperEl = popperRef.value
    onInvalidate(() => {
      eventRegOrUnReg(false)
    })
    if (referenceEl && popperEl) {
      eventRegOrUnReg(true)
    }
  })

  onUnmounted(() => {
    if (state.instance) {
      state.instance.destroy()
    }
  })

  return state
}