UNPKG

@gitlab/ui

Version:
263 lines (248 loc) • 9.59 kB
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 _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } 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 _rgb$map = rgb.map(val => parseInt(val, 16)), _rgb$map2 = _slicedToArray(_rgb$map, 3), r = _rgb$map2[0], g = _rgb$map2[1], b = _rgb$map2[2]; return [r, g, b]; } function rgbFromString(color, sub) { const rgb = color.substring(sub, color.length - 1).split(COMMA); const _rgb$map3 = rgb.map(i => parseInt(i, 10)), _rgb$map4 = _slicedToArray(_rgb$map3, 3), r = _rgb$map4[0], g = _rgb$map4[1], b = _rgb$map4[2]; return [r, g, b]; } function hexToRgba(hex) { let opacity = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; const _rgbFromHex = rgbFromHex(hex), _rgbFromHex2 = _slicedToArray(_rgbFromHex, 3), r = _rgbFromHex2[0], g = _rgbFromHex2[1], b = _rgbFromHex2[2]; 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 = _ref.min, max = _ref.max; 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.tagName; 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 {Object} [context] Optional object with additional context. * @param {string} [context.name] The name of the context of the message. Usually the component's name. * @param {HTMLElement} [context.element] The element relevant to the message. */ function logWarning(message) { let context = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (isDev()) { const name = context.name, element = context.element; const formattedMessage = name ? `[${name}] ${message}` : message; const args = element ? [formattedMessage, element] : [formattedMessage]; console.warn(...args); // eslint-disable-line no-console } } /** * Stop default event handling and propagation */ function stopEvent(event) { let _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, _ref2$preventDefault = _ref2.preventDefault, preventDefault = _ref2$preventDefault === void 0 ? true : _ref2$preventDefault, _ref2$stopPropagation = _ref2.stopPropagation, stopPropagation = _ref2$stopPropagation === void 0 ? true : _ref2$stopPropagation, _ref2$stopImmediatePr = _ref2.stopImmediatePropagation, stopImmediatePropagation = _ref2$stopImmediatePr === void 0 ? false : _ref2$stopImmediatePr; 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 };