medialize/ally.js

View on GitHub
src/get/focus-redirect-target.js

Summary

Maintainability
A
55 mins
Test Coverage

import isFocusable from '../is/focusable';
import queryFocusable from '../query/focusable';
import queryTabbable from '../query/tabbable';
import contextToElement from '../util/context-to-element';
import getDocument from '../util/get-document';
import mergeInDomOrder from '../util/merge-dom-order';
import {getMapOfImage} from '../util/image-map';

import _supports from '../supports/supports';
let supports;

function formControlElement(element) {
  const nodeName = element.nodeName.toLowerCase();
  return nodeName === 'input'
    || nodeName === 'textarea'
    || nodeName === 'select'
    || nodeName === 'button';
}

function resolveLabelElement(element, _document) {
  const forId = element.getAttribute('for');
  if (forId) {
    // <label for="…"> - referenced form control
    return _document.getElementById(forId);
  }

  // <label><input - nested form control
  return element.querySelector('input, select, textarea');
}

function resolveLegendWithinFieldset(element) {
  // Chrome: first focusable input/select/textarea/button within <fieldset>
  const fieldset = element.parentNode;
  const focusable = queryFocusable({
    context: fieldset,
    strategy: 'strict',
  });

  return focusable.filter(formControlElement)[0] || null;
}

function resolveLegendWithinDocument(element, _document) {
  // Firefox: *next* tabbable (in DOM order)
  const tabbable = queryTabbable({
    // Firefox apparently needs to query from the body element,
    // not the document, looking inside a dynamically written iframe
    context: _document.body,
    strategy: 'strict',
  });

  if (!tabbable.length) {
    return null;
  }

  // sort <legend> into the list of tabbable elements
  // so that we can identify the next element
  const merged = mergeInDomOrder({
    list: tabbable,
    elements: [element],
  });

  const offset = merged.indexOf(element);
  if (offset === merged.length - 1) {
    return null;
  }

  return merged[offset + 1];
}

function resolveLegendElement(element, _document) {
  // <legend> - first <input> in <fieldset>
  if (!supports.focusRedirectLegend) {
    return null;
  }

  // legend must be the first child of a <fieldset>
  const fieldset = element.parentNode;
  if (fieldset.nodeName.toLowerCase() !== 'fieldset') {
    return null;
  }

  if (supports.focusRedirectLegend === 'tabbable') {
    // Firefox goes for *next* tabbable (in DOM order)
    return resolveLegendWithinDocument(element, _document);
  }

  // Chrome goes for first focusable input/select/textarea/button within <fieldset>
  return resolveLegendWithinFieldset(element, _document);
}

function resolveImgElement(element) {
  if (!supports.focusRedirectImgUsemap) {
    return null;
  }

  // IE9-11: <img usemap="#…" src="…"> - first <area>
  const map = getMapOfImage(element);
  return map && map.querySelector('area') || null;
}

export default function({context, skipFocusable} = {}) {
  if (!supports) {
    supports = _supports();
  }

  const element = contextToElement({
    label: 'get/focus-redirect-target',
    context,
  });

  if (!skipFocusable && isFocusable(element)) {
    return null;
  }

  const nodeName = element.nodeName.toLowerCase();
  const _document = getDocument(element);

  if (nodeName === 'label') {
    return resolveLabelElement(element, _document);
  }

  if (nodeName === 'legend') {
    return resolveLegendElement(element, _document);
  }

  if (nodeName === 'img') {
    return resolveImgElement(element, _document);
  }

  return null;
}