All files / features/dropdown-reflects-select-position-and-visibility dropdown-position-updater.js

75% Statements 96/128
76.92% Branches 10/13
57.14% Functions 8/14
75% Lines 96/128

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 1291x 1x 1x 1x 1x 1x 1x 1x 4x 4x 4x 4x 4x     4x 1x 1x 1x 1x 1x 1x 1x 4x 4x 4x 4x 4x 4x 4x 1x 1x 1x 1x 1x 1x 1x 4x 4x 4x 4x 4x   4x 4x 4x 4x               4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x       4x 4x 4x 4x 4x 4x 4x 4x 4x 4x         4x 4x 4x 1x 1x 1x 1x 1x 1x 4x 4x 4x 4x 4x               4x 4x 4x 4x 4x 4x 1x 1x 1x 1x                 1x 1x 1x 1x 1x 1x 1x  
import { dropdownEl, inputEl, isDynamicSelect } from '../../utils/dynamic-select-dom.js'
import { centerDropdownPosition, shouldCenterDropdown } from '../centers-dropdown-to-screen-on-mobile/dropdown-mobile-centering.js'
/** @import {DynamicSelect} from '../../utils/dynamic-select-dom' */
 
/** @type {WeakMap<DynamicSelect, DropdownPositionUpdater>} */
const dataLoaderData = new WeakMap()
 
const intersectionObserver = new IntersectionObserver(records => {
  const selects = new Set(Iterator.from(records)
    .filter(record => !record.isIntersecting)
    .map(record => record.target)
    .filter(isDynamicSelect))
  selects.forEach(select => {
    select.open = false
    dropdownPositionUpdaterOf(select).stopAnchoringToSelect()
  })
})
 
/**
 * @param {DynamicSelect} element - target element
 * @returns {DropdownPositionUpdater} dataloader of element
 */
export function dropdownPositionUpdaterOf (element) {
  let dataLoader = dataLoaderData.get(element)
  if (!dataLoader) {
    dataLoader = createDropdownPositionUpdaterFor(new WeakRef(element))
    dataLoaderData.set(element, dataLoader)
  }
  return dataLoader
}
 
/**
 * Creates a dataloader for an element
 * @param {WeakRef<DynamicSelect>} elementRef - weak reference of element. We do not want to have any strong reference chain pointing to
 * globally allocated `dataLoaderData`, effectively creating a memory leak
 * @returns {DropdownPositionUpdater} - created dataloader for element
 */
function createDropdownPositionUpdaterFor (elementRef) {
  const getElement = () => {
    const element = elementRef.deref()
    if (!element) {
      removeListenersForPotentialPositionChange()
      throw Error('element no longer exists')
    }
    return element
  }
  const potentialPositionChangeCallback = () => {
    const element = getElement()
    if (!element.open) {
      api.stopAnchoringToSelect()
      return
    }
    updateDropdownPosition(element)
  }
 
  const addScrollListener = () => document.defaultView?.addEventListener('scroll', potentialPositionChangeCallback, true)
  const removeScrollListener = () => document.defaultView?.removeEventListener('scroll', potentialPositionChangeCallback, true)
  const addResizeListener = () => document.defaultView?.addEventListener('resize', potentialPositionChangeCallback)
  const removeResizeListener = () => document.defaultView?.removeEventListener('resize', potentialPositionChangeCallback)
  const addListenersForPotentialPositionChange = () => {
    addScrollListener()
    addResizeListener()
  }
 
  const removeListenersForPotentialPositionChange = () => {
    removeScrollListener()
    removeResizeListener()
  }
 
  /** @type {DropdownPositionUpdater} */
  const api = {
    startAnchoringToSelect: () => {
      const element = getElement()
      updateDropdownPosition(element)
      addListenersForPotentialPositionChange()
      intersectionObserver.observe(element)
    },
    stopAnchoringToSelect () {
      const element = getElement()
      intersectionObserver.unobserve(element)
      removeListenersForPotentialPositionChange()
    },
  }
  return api
}
 
/**
 * Update dropdown content based on the content in dynamic select in light DOM
 * @param {DynamicSelect} dynamicSelect - web component element reference
 */
export function updateDropdownPosition (dynamicSelect) {
  if (shouldCenterDropdown()) {
    centerDropdownPosition(dynamicSelect)
    return
  }
  const dropdown = dropdownEl(dynamicSelect)
  const input = inputEl(dynamicSelect)
  const clientRect = input.getBoundingClientRect()
  dropdown.style.minWidth = `${clientRect.width}px`
  const viewportRect = getViewportRect()
  const isTopDirection = clientRect.bottom + dropdown.clientHeight > viewportRect.height
  const isLeftDirection = clientRect.left + dropdown.clientWidth > viewportRect.width
  const xPosition = isTopDirection ? clientRect.top : clientRect.bottom
  const yPosition = isLeftDirection ? Math.min(viewportRect.width, clientRect.right) : Math.max(0, clientRect.left)
  dropdown.classList.toggle('top-direction', isTopDirection)
  dropdown.classList.toggle('left-direction', isLeftDirection)
  dropdown.style.marginTop = `${xPosition}px`
  dropdown.style.marginLeft = `${yPosition}px`
}
 
/**
 * @returns {DOMRectReadOnly} viewport rect
 */
function getViewportRect () {
  const { visualViewport } = window
  if (!visualViewport) {
    return new DOMRectReadOnly(0, 0, window.innerWidth, window.innerHeight)
  }
  const { offsetLeft, offsetTop, width, height } = visualViewport
  return new DOMRectReadOnly(offsetLeft, offsetTop, width, height)
}
 
/**
 * @typedef {object} DropdownPositionUpdater
 * @property {() => void} startAnchoringToSelect - starts anchoring to select until select is not visible,
 *   at that point it closes the dropdown
 * @property {() => void} stopAnchoringToSelect - stop anchoring to select
 */