@gitlab/ui
Version: 
GitLab UI Components
199 lines (185 loc) • 6.98 kB
JavaScript
import { isVisible } from '../vendor/bootstrap-vue/src/utils/dom';
export { isVisible } from '../vendor/bootstrap-vue/src/utils/dom';
import { COMMA, labelColorOptions, CONTRAST_LEVELS, focusableTags } from './constants';
function debounceByAnimationFrame(fn) {
  let requestId;
  return function debounced() {
    for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }
    if (requestId) {
      window.cancelAnimationFrame(requestId);
    }
    requestId = window.requestAnimationFrame(() => fn.apply(this, args));
  };
}
function throttle(fn) {
  let frameId = null;
  return function () {
    for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
      args[_key2] = arguments[_key2];
    }
    if (frameId) {
      return;
    }
    frameId = window.requestAnimationFrame(() => {
      fn(...args);
      frameId = null;
    });
  };
}
function rgbFromHex(hex) {
  const cleanHex = hex.replace('#', '');
  const rgb = cleanHex.length === 3 ? cleanHex.split('').map(val => val + val) : cleanHex.match(/[\da-f]{2}/gi);
  const [r, g, b] = rgb.map(val => parseInt(val, 16));
  return [r, g, b];
}
function rgbFromString(color, sub) {
  const rgb = color.substring(sub, color.length - 1).split(COMMA);
  const [r, g, b] = rgb.map(i => parseInt(i, 10));
  return [r, g, b];
}
function hexToRgba(hex) {
  let opacity = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
  const [r, g, b] = rgbFromHex(hex);
  return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}
function toSrgb(value) {
  const normalized = value / 255;
  return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
}
function relativeLuminance(rgb) {
  // WCAG 2.1 formula: https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
  // -
  // WCAG 3.0 will use APAC
  // Using APAC would be the ultimate goal, but was dismissed by engineering as of now
  // See https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3418#note_1370107090
  return 0.2126 * toSrgb(rgb[0]) + 0.7152 * toSrgb(rgb[1]) + 0.0722 * toSrgb(rgb[2]);
}
function colorFromBackground(backgroundColor) {
  let contrastRatio = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2.4;
  let color;
  const lightColor = rgbFromHex('#FFFFFF');
  const darkColor = rgbFromHex('#18171d');
  if (backgroundColor.startsWith('#')) {
    color = rgbFromHex(backgroundColor);
  } else if (backgroundColor.startsWith('rgba(')) {
    color = rgbFromString(backgroundColor, 5);
  } else if (backgroundColor.startsWith('rgb(')) {
    color = rgbFromString(backgroundColor, 4);
  }
  const luminance = relativeLuminance(color);
  const lightLuminance = relativeLuminance(lightColor);
  const darkLuminance = relativeLuminance(darkColor);
  const contrastLight = (lightLuminance + 0.05) / (luminance + 0.05);
  const contrastDark = (luminance + 0.05) / (darkLuminance + 0.05);
  // Using a default threshold contrast of 2.4 instead of 3
  // as this will solve weird color combinations in the mid tones
  return contrastLight >= contrastRatio || contrastLight > contrastDark ? labelColorOptions.light : labelColorOptions.dark;
}
function getColorContrast(foreground, background) {
  // Formula: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
  const backgroundLuminance = relativeLuminance(rgbFromHex(background)) + 0.05;
  const foregroundLuminance = relativeLuminance(rgbFromHex(foreground)) + 0.05;
  let score = backgroundLuminance / foregroundLuminance;
  if (foregroundLuminance > backgroundLuminance) {
    score = 1 / score;
  }
  const level = CONTRAST_LEVELS.find(_ref => {
    let {
      min,
      max
    } = _ref;
    return score >= min && score < max;
  });
  return {
    score: (Math.round(score * 10) / 10).toFixed(1),
    level
  };
}
function uid() {
  return Math.random().toString(36).substring(2);
}
/**
 * Receives an element and validates that it can be focused
 * @param { HTMLElement } The element we want to validate
 * @return { boolean } Is the element focusable
 */
function isElementFocusable(elt) {
  if (!elt) return false;
  const {
    tagName
  } = elt;
  const isValidTag = focusableTags.includes(tagName);
  const hasValidType = elt.getAttribute('type') !== 'hidden';
  const isDisabled = elt.getAttribute('disabled') === '' || elt.getAttribute('disabled');
  const hasValidZIndex = elt.getAttribute('z-index') !== '-1';
  const isInvalidAnchorTag = tagName === 'A' && !elt.getAttribute('href');
  return isValidTag && hasValidType && !isDisabled && hasValidZIndex && !isInvalidAnchorTag;
}
/**
 * Receives an element and validates that it is reachable via sequential keyboard navigation
 * @param { HTMLElement } The element to validate
 * @return { boolean } Is the element focusable in a sequential tab order
 */
function isElementTabbable(el) {
  if (!el) return false;
  const tabindex = parseInt(el.getAttribute('tabindex'), 10);
  return tabindex > -1;
}
/**
 * Receives an array of HTML elements and focus the first one possible
 * @param { Array.<HTMLElement> } An array of element to potentially focus
 * @return { undefined }
 */
function focusFirstFocusableElement(elts) {
  const focusableElt = elts.find(el => isElementFocusable(el));
  if (focusableElt) focusableElt.focus();
}
/**
 * Returns true if the current environment is considered a development environment (it's not
 * production or test).
 *
 * @returns {boolean}
 */
function isDev() {
  return !['test', 'production'].includes(process.env.NODE_ENV);
}
/**
 * Prints a warning message to the console in non-test and non-production environments.
 * @param {string} message message to print to the console
 * @param {HTMLElement} element component that triggered the warning
 */
function logWarning() {
  let message = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
  let element = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
  if (message.length && isDev()) {
    console.warn(message, element); // eslint-disable-line no-console
  }
}
/**
 * Stop default event handling and propagation
 */
function stopEvent(event) {
  let {
    preventDefault = true,
    stopPropagation = true,
    stopImmediatePropagation = false
  } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  if (preventDefault) {
    event.preventDefault();
  }
  if (stopPropagation) {
    event.stopPropagation();
  }
  if (stopImmediatePropagation) {
    event.stopImmediatePropagation();
  }
}
/**
 * Return an Array of visible items
 */
function filterVisible(els) {
  return (els || []).filter(el => isVisible(el));
}
export { colorFromBackground, debounceByAnimationFrame, filterVisible, focusFirstFocusableElement, getColorContrast, hexToRgba, isDev, isElementFocusable, isElementTabbable, logWarning, relativeLuminance, rgbFromHex, rgbFromString, stopEvent, throttle, toSrgb, uid };