"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = useArrowNav;
exports.getClosestTabbable = getClosestTabbable;
exports.isNavigationCandidate = isNavigationCandidate;
var _dom = require("@wordpress/dom");
var _keycodes = require("@wordpress/keycodes");
var _data = require("@wordpress/data");
var _compose = require("@wordpress/compose");
var _dom2 = require("../../utils/dom");
var _store = require("../../store");
/**
 * WordPress dependencies
 */

/**
 * Internal dependencies
 */

/**
 * Returns true if the element should consider edge navigation upon a keyboard
 * event of the given directional key code, or false otherwise.
 *
 * @param {Element} element     HTML element to test.
 * @param {number}  keyCode     KeyboardEvent keyCode to test.
 * @param {boolean} hasModifier Whether a modifier is pressed.
 *
 * @return {boolean} Whether element should consider edge navigation.
 */
function isNavigationCandidate(element, keyCode, hasModifier) {
  const isVertical = keyCode === _keycodes.UP || keyCode === _keycodes.DOWN;
  const {
    tagName
  } = element;
  const elementType = element.getAttribute('type');

  // Native inputs should not navigate vertically, unless they are simple types that don't need up/down arrow keys.
  if (isVertical && !hasModifier) {
    if (tagName === 'INPUT') {
      const verticalInputTypes = ['date', 'datetime-local', 'month', 'number', 'range', 'time', 'week'];
      return !verticalInputTypes.includes(elementType);
    }
    return true;
  }

  // Native inputs should not navigate horizontally, unless they are simple types that don't need left/right arrow keys.
  if (tagName === 'INPUT') {
    const simpleInputTypes = ['button', 'checkbox', 'number', 'color', 'file', 'image', 'radio', 'reset', 'submit'];
    return simpleInputTypes.includes(elementType);
  }

  // Native textareas should not navigate horizontally.
  return tagName !== 'TEXTAREA';
}

/**
 * Returns the optimal tab target from the given focused element in the desired
 * direction. A preference is made toward text fields, falling back to the block
 * focus stop if no other candidates exist for the block.
 *
 * @param {Element} target           Currently focused text field.
 * @param {boolean} isReverse        True if considering as the first field.
 * @param {Element} containerElement Element containing all blocks.
 * @param {boolean} onlyVertical     Whether to only consider tabbable elements
 *                                   that are visually above or under the
 *                                   target.
 *
 * @return {?Element} Optimal tab target, if one exists.
 */
function getClosestTabbable(target, isReverse, containerElement, onlyVertical) {
  // Since the current focus target is not guaranteed to be a text field, find
  // all focusables. Tabbability is considered later.
  let focusableNodes = _dom.focus.focusable.find(containerElement);
  if (isReverse) {
    focusableNodes.reverse();
  }

  // Consider as candidates those focusables after the current target. It's
  // assumed this can only be reached if the target is focusable (on its
  // keydown event), so no need to verify it exists in the set.
  focusableNodes = focusableNodes.slice(focusableNodes.indexOf(target) + 1);
  let targetRect;
  if (onlyVertical) {
    targetRect = target.getBoundingClientRect();
  }
  function isTabCandidate(node) {
    if (node.closest('[inert]')) {
      return;
    }

    // Skip if there's only one child that is content editable (and thus a
    // better candidate).
    if (node.children.length === 1 && (0, _dom2.isInSameBlock)(node, node.firstElementChild) && node.firstElementChild.getAttribute('contenteditable') === 'true') {
      return;
    }

    // Not a candidate if the node is not tabbable.
    if (!_dom.focus.tabbable.isTabbableIndex(node)) {
      return false;
    }

    // Skip focusable elements such as links within content editable nodes.
    if (node.isContentEditable && node.contentEditable !== 'true') {
      return false;
    }
    if (onlyVertical) {
      const nodeRect = node.getBoundingClientRect();
      if (nodeRect.left >= targetRect.right || nodeRect.right <= targetRect.left) {
        return false;
      }
    }
    return true;
  }
  return focusableNodes.find(isTabCandidate);
}
function useArrowNav() {
  const {
    getMultiSelectedBlocksStartClientId,
    getMultiSelectedBlocksEndClientId,
    getSettings,
    hasMultiSelection,
    __unstableIsFullySelected
  } = (0, _data.useSelect)(_store.store);
  const {
    selectBlock
  } = (0, _data.useDispatch)(_store.store);
  return (0, _compose.useRefEffect)(node => {
    // Here a DOMRect is stored while moving the caret vertically so
    // vertical position of the start position can be restored. This is to
    // recreate browser behaviour across blocks.
    let verticalRect;
    function onMouseDown() {
      verticalRect = null;
    }
    function isClosestTabbableABlock(target, isReverse) {
      const closestTabbable = getClosestTabbable(target, isReverse, node);
      return closestTabbable && (0, _dom2.getBlockClientId)(closestTabbable);
    }
    function onKeyDown(event) {
      // Abort if navigation has already been handled (e.g. RichText
      // inline boundaries).
      if (event.defaultPrevented) {
        return;
      }
      const {
        keyCode,
        target,
        shiftKey,
        ctrlKey,
        altKey,
        metaKey
      } = event;
      const isUp = keyCode === _keycodes.UP;
      const isDown = keyCode === _keycodes.DOWN;
      const isLeft = keyCode === _keycodes.LEFT;
      const isRight = keyCode === _keycodes.RIGHT;
      const isReverse = isUp || isLeft;
      const isHorizontal = isLeft || isRight;
      const isVertical = isUp || isDown;
      const isNav = isHorizontal || isVertical;
      const hasModifier = shiftKey || ctrlKey || altKey || metaKey;
      const isNavEdge = isVertical ? _dom.isVerticalEdge : _dom.isHorizontalEdge;
      const {
        ownerDocument
      } = node;
      const {
        defaultView
      } = ownerDocument;
      if (!isNav) {
        return;
      }

      // If there is a multi-selection, the arrow keys should collapse the
      // selection to the start or end of the selection.
      if (hasMultiSelection()) {
        if (shiftKey) {
          return;
        }

        // Only handle if we have a full selection (not a native partial
        // selection).
        if (!__unstableIsFullySelected()) {
          return;
        }
        event.preventDefault();
        if (isReverse) {
          selectBlock(getMultiSelectedBlocksStartClientId());
        } else {
          selectBlock(getMultiSelectedBlocksEndClientId(), -1);
        }
        return;
      }

      // Abort if our current target is not a candidate for navigation
      // (e.g. preserve native input behaviors).
      if (!isNavigationCandidate(target, keyCode, hasModifier)) {
        return;
      }

      // When presing any key other than up or down, the initial vertical
      // position must ALWAYS be reset. The vertical position is saved so
      // it can be restored as well as possible on sebsequent vertical
      // arrow key presses. It may not always be possible to restore the
      // exact same position (such as at an empty line), so it wouldn't be
      // good to compute the position right before any vertical arrow key
      // press.
      if (!isVertical) {
        verticalRect = null;
      } else if (!verticalRect) {
        verticalRect = (0, _dom.computeCaretRect)(defaultView);
      }

      // In the case of RTL scripts, right means previous and left means
      // next, which is the exact reverse of LTR.
      const isReverseDir = (0, _dom.isRTL)(target) ? !isReverse : isReverse;
      const {
        keepCaretInsideBlock
      } = getSettings();
      if (shiftKey) {
        if (isClosestTabbableABlock(target, isReverse) && isNavEdge(target, isReverse)) {
          node.contentEditable = true;
          // Firefox doesn't automatically move focus.
          node.focus();
        }
      } else if (isVertical && (0, _dom.isVerticalEdge)(target, isReverse) && (
      // When Alt is pressed, only intercept if the caret is also at
      // the horizontal edge.
      altKey ? (0, _dom.isHorizontalEdge)(target, isReverseDir) : true) && !keepCaretInsideBlock) {
        const closestTabbable = getClosestTabbable(target, isReverse, node, true);
        if (closestTabbable) {
          (0, _dom.placeCaretAtVerticalEdge)(closestTabbable,
          // When Alt is pressed, place the caret at the furthest
          // horizontal edge and the furthest vertical edge.
          altKey ? !isReverse : isReverse, altKey ? undefined : verticalRect);
          event.preventDefault();
        }
      } else if (isHorizontal && defaultView.getSelection().isCollapsed && (0, _dom.isHorizontalEdge)(target, isReverseDir) && !keepCaretInsideBlock) {
        const closestTabbable = getClosestTabbable(target, isReverseDir, node);
        (0, _dom.placeCaretAtHorizontalEdge)(closestTabbable, isReverse);
        event.preventDefault();
      }
    }
    node.addEventListener('mousedown', onMouseDown);
    node.addEventListener('keydown', onKeyDown);
    return () => {
      node.removeEventListener('mousedown', onMouseDown);
      node.removeEventListener('keydown', onKeyDown);
    };
  }, []);
}
//# sourceMappingURL=use-arrow-nav.js.map