@popperjs/core#VirtualElement TypeScript Examples

The following examples show how to use @popperjs/core#VirtualElement. 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: 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 #2
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,
  }
}