UNPKG

azure-devops-ui

Version:

React components for building web UI in Azure DevOps

524 lines (523 loc) 20.2 kB
import * as React from "react"; /** * Set of KeyCodes that are used in the platform. */ export var KeyCode; (function (KeyCode) { KeyCode[KeyCode["backspace"] = 8] = "backspace"; KeyCode[KeyCode["tab"] = 9] = "tab"; KeyCode[KeyCode["enter"] = 13] = "enter"; KeyCode[KeyCode["shift"] = 16] = "shift"; KeyCode[KeyCode["ctrl"] = 17] = "ctrl"; KeyCode[KeyCode["alt"] = 18] = "alt"; KeyCode[KeyCode["pause"] = 19] = "pause"; KeyCode[KeyCode["capsLock"] = 20] = "capsLock"; KeyCode[KeyCode["escape"] = 27] = "escape"; KeyCode[KeyCode["space"] = 32] = "space"; KeyCode[KeyCode["pageUp"] = 33] = "pageUp"; KeyCode[KeyCode["pageDown"] = 34] = "pageDown"; KeyCode[KeyCode["end"] = 35] = "end"; KeyCode[KeyCode["home"] = 36] = "home"; KeyCode[KeyCode["leftArrow"] = 37] = "leftArrow"; KeyCode[KeyCode["upArrow"] = 38] = "upArrow"; KeyCode[KeyCode["rightArrow"] = 39] = "rightArrow"; KeyCode[KeyCode["downArrow"] = 40] = "downArrow"; KeyCode[KeyCode["delete"] = 46] = "delete"; KeyCode[KeyCode["b"] = 66] = "b"; KeyCode[KeyCode["i"] = 73] = "i"; KeyCode[KeyCode["k"] = 75] = "k"; KeyCode[KeyCode["q"] = 81] = "q"; KeyCode[KeyCode["t"] = 84] = "t"; KeyCode[KeyCode["windowsKey"] = 91] = "windowsKey"; KeyCode[KeyCode["macCommand"] = 91] = "macCommand"; KeyCode[KeyCode["F10"] = 121] = "F10"; KeyCode[KeyCode["numLock"] = 144] = "numLock"; KeyCode[KeyCode["scrollLock"] = 145] = "scrollLock"; KeyCode[KeyCode["comma"] = 188] = "comma"; })(KeyCode || (KeyCode = {})); /** * Determines whether or not a keystroke is an arrow key or not. */ export function isArrowKey(event) { return (event.which === KeyCode.downArrow || event.which === KeyCode.upArrow || event.which === KeyCode.leftArrow || event.which === KeyCode.rightArrow); } /** * Type guard function to determine if children are defined as a function * @param children (usually from this.props.children) */ export function isFunctionalChildren(children) { return typeof children === "function"; } /** * childCount is used to determine the number of defined renderable children within * a standard set of React.Children. This is different than React.Children.length * which includes children that are null or undefined. */ export function childCount(children) { let childCount = 0; React.Children.forEach(children, function (child) { if (child) { childCount++; } }); return childCount; } /** * getSafeId is designed to create a string from the input id that is safe for use * as the id attribute of a component. The ids appear in the global javscript namespace. * This means if you create an element and assign the "id" property to a value * the element is accessible by doing window.<id>. This causes problems when the * id of the element collides with other global objects. Using a SafeId adds a prefix * intended to avoid conflicts. * * This should be called anytime a DOM elements property is being set that refers to * the components id. This should not be called when passing the id as a prop to a * component. It is the components responsibility to make the Id safe when attaching * it to an element. * * This includes but is not limited to properties like: * aria-controls, aria-describedby, aria-labelledby, id, htmlFor, ... * * @param id The root id that is being made "Safe". */ export function getSafeId(id) { if (false) { if (id && id.startsWith("__bolt-")) { console.error(`getSafeId was called twice on id ${id}, it should only be called once`); } } // querySelector won't select id's with .'s in them replace them with '-'. return id ? "__bolt-" + id.replace(/[^0-9A-Za-z_]/g, "-") : undefined; } /** * getSafeIdSelector will return the string that can use used to denote the selector * for elements that use this id. * * @param id The root id that is being made "Safe". */ export function getSafeIdSelector(id) { return "#" + getSafeId(id); } /** * function that does nothing and accepts any set of arguments. */ export function noop() { } /** * Basic function for building a css classlist string from and array of classes, where * one of more of the arguments may be null or undefined. * * @param args Array of strings the represents the css class list. * * @example css("base", "active", x === 42 && "optional") will return "base active optional" if x === 42 or "base active" otherwise */ export function css(...args) { const classes = []; for (let arg of args) { if (arg) { if (typeof arg === 'string') { classes.push(arg); } else if (arg.hasOwnProperty('toString') && typeof arg.toString === 'function') { classes.push(arg.toString()); } else { for (let key in arg) { if (arg[key]) { classes.push(key); } } } } } return classes .filter(c => c) .join(" ") .trim(); } /** * Returns the set of parent elements with index 0 the root and the last * element is either the direct parent or itself based on includeSelf. * * @param element The element to get the parent element hierarchy from. * @param includeSelf Should the element supplied be included in the parent list. * @param rootElement Optional root element to stop processing * @param includeRoot Should the root element supplied be included in the parent list. */ export function getParents(element, includeSelf, rootElement, includeRoot) { const parentElements = []; if (includeSelf) { parentElements.push(element); } while (element.parentElement && element.parentElement !== rootElement) { parentElements.splice(0, 0, element.parentElement); element = element.parentElement; } if (element.parentElement && includeRoot) { parentElements.splice(0, 0, element.parentElement); } return parentElements; } /** * Determines if the target element of an event (or its ancestry) has a particular node name. * * @param event The initial element is pulled off of this event. * @param nodeNames A list of DOM node names ("A", "INPUT", etc.) to check for the presence * @param rootAncestor If provided, build a list of ancestors from the event's element, to this element to check. Otherwise, * only check the element from the event. */ export function eventTargetContainsNode(event, nodeNames, rootAncestor) { const targetElement = event.target; const ancestors = rootAncestor ? getParents(targetElement, true, rootAncestor, true) : [targetElement]; return ancestors.some(element => nodeNames.indexOf(element.nodeName) !== -1); } /** * ElementRelationship is used to define how two elements in the same * document are related in position to each other. */ export var ElementRelationship; (function (ElementRelationship) { ElementRelationship[ElementRelationship["Unrelated"] = 0] = "Unrelated"; ElementRelationship[ElementRelationship["Before"] = 1] = "Before"; ElementRelationship[ElementRelationship["After"] = 2] = "After"; ElementRelationship[ElementRelationship["Child"] = 3] = "Child"; ElementRelationship[ElementRelationship["Parent"] = 4] = "Parent"; })(ElementRelationship || (ElementRelationship = {})); /** * getRelationship returns the relationship of the two specified elements. * * @param element1 * @param element2 */ export function getRelationship(element1, element2) { // If the second element is a child of the first element, then element1 occurs before element2. if (element1.contains(element2)) { return ElementRelationship.Parent; } // If the first element is a child of the second element, then element1 occurs after element2. if (element2.contains(element1)) { return ElementRelationship.Child; } // Retrieve the parents of both the elements. const parents1 = getParents(element1, true); const parents2 = getParents(element2, true); for (let elementIndex = 0;; elementIndex++) { if (parents1[elementIndex] !== parents2[elementIndex]) { const siblings = parents1[elementIndex - 1].children; for (let siblingIndex = 0; siblingIndex < siblings.length; siblingIndex++) { if (siblings[siblingIndex] === parents1[elementIndex]) { return ElementRelationship.Before; } if (siblings[siblingIndex] === parents2[elementIndex]) { return ElementRelationship.After; } } } } } /** * preventDefault is used as a standard delegate to prevent the default behavior * for a given event. * * @param event Synthetic event that should have its default action prevented. */ export function preventDefault(event) { event.preventDefault(); } /** * shimRef is used to acquire a React Ref from a child component. If the child * has an existing ref, it will return the existing ref, if not it will * create a new one. */ export function shimRef(child) { // @HACK: This uses an internal property on the created element which is the // forwarded ref property of the element. If React ever changes the implementation // removing this property this code will need to be updated. // @NOTE: The ref MUST be a React.createRef if the a ref property is specified, // otherwise we will not be able to share the ref. let ref = child.ref; // If no ref was created by the element owner we will add one. if (!ref) { ref = React.createRef(); } else { // @DEBUG: Ensure the ref is a React.createRef by validated the current property if (!ref.hasOwnProperty("current")) { throw Error("Children of a focus zone MUST use React.createRef to obtain child references"); } // @DEBUG } return ref; } /** * Set up a ref resolver function given internal state managed for the ref. * Taken from FluentUI v8 and modified to match local style * @param local Set */ function createResolver(local) { return (newValue) => { for (const ref of local.refs) { if (typeof ref === "function") { ref(newValue); } else if (ref) { // work around the immutability of the React.Ref type ref.current = newValue; } } }; } /** * Helper to merge refs from within class components. * Taken from FluentUI v8 and modified to match local style */ export function createMergedRef(value) { const local = { refs: [], }; return (...newRefs) => { if (!local.resolver || !arrayEquals(local.refs, newRefs)) { local.resolver = createResolver(local); } local.refs = newRefs; return local.resolver; }; } let focusVisible = false; /** * Determine whether or not focus is currently visible to the user. This generally * means the user is using the keyboard to manage focus instead of the mouse. */ export function getFocusVisible() { return focusVisible; } /** * Make sure the focus treatment is enabled and disabled based on * the state of mouse and keyboard usage. */ export function setFocusVisible(visible) { if ((focusVisible = visible) === true) { document.body && document.body.classList.add("bolt-focus-visible"); } else { document.body && document.body.classList.remove("bolt-focus-visible"); } } /* Setup the set of non-focus keys, when these are pressed it doesnt start showing focus treatment */ const nonFocusKeys = new Array(255); nonFocusKeys[KeyCode.alt] = true; nonFocusKeys[KeyCode.capsLock] = true; nonFocusKeys[KeyCode.ctrl] = true; nonFocusKeys[KeyCode.numLock] = true; nonFocusKeys[KeyCode.pause] = true; nonFocusKeys[KeyCode.scrollLock] = true; nonFocusKeys[KeyCode.shift] = true; nonFocusKeys[KeyCode.windowsKey] = true; document.addEventListener("keydown", (event) => { if (!nonFocusKeys[event.which]) { setFocusVisible(true); } }, true); let mouseCapture; // MouseCaptureFunction is the global mouse handler we use to trap events and forward // them to the current capture if one exists. const mouseCaptureFunction = (event) => { // Track the position of the mouse as it moves. Mouse.position.x = event.clientX; Mouse.position.y = event.clientY; // Notify the mouse capture of the mouse movement and mouseup if one is signed up. if (mouseCapture && mouseCapture.callback && mouseCapture.button === event.button) { mouseCapture.callback(event); if (event.type === "mouseup") { Mouse.releaseCapture(mouseCapture.callback); } } }; export const Mouse = { position: { x: 0, y: 0 }, releaseCapture: function releaseCapture(callback) { if (mouseCapture && mouseCapture.callback === callback) { mouseCapture = undefined; } }, setCapture: function setCapture(callback, button = 0) { // Before starting a new capture, we will release the current capture. if (mouseCapture) { Mouse.releaseCapture(mouseCapture.callback); } // Update the mouseCapture to the new capture. mouseCapture = { button, callback }; } }; document.addEventListener("mousemove", mouseCaptureFunction); document.addEventListener("mouseup", mouseCaptureFunction); document.addEventListener("mousedown", event => { // Screen readers on scan mode trigger some key strokes as Mouse events. // We can easily identify those events because they have no coordinates. if (event.button === 0 && event.clientX === 0 && event.clientY === 0 && event.screenX === 0 && event.screenY === 0 && event.pageX === 0 && event.pageY === 0) { return; } setFocusVisible(false); }, true); let touchCapture; // touchCaptureFunction is the global touch handler we use to trap events and forward // them to the current capture if one exists. const touchCaptureFunction = (event) => { const touch = event.changedTouches && event.changedTouches.length ? event.changedTouches[0] : event.touches[0]; // Track the position of the touch as it moves. Touch.position.x = touch.clientX; Touch.position.y = touch.clientY; // Notify the touch capture of the touch movement and touchend if one is signed up. if (touchCapture && touchCapture.callback) { touchCapture.callback(event); if (event.type === "touchend") { Touch.releaseCapture(touchCapture.callback); } } }; /** * Currently only basic touch support - assumes a single touch * throughout the touch operation. */ export const Touch = { position: { x: 0, y: 0 }, releaseCapture: function releaseCapture(callback) { if (touchCapture && touchCapture.callback === callback) { touchCapture = undefined; } }, setCapture: function setCapture(callback) { // Before starting a new capture, we will release the current capture. if (touchCapture) { Touch.releaseCapture(touchCapture.callback); } // Update the touchCapture to the new capture. touchCapture = { callback }; } }; document.addEventListener("touchmove", touchCaptureFunction); document.addEventListener("touchend", touchCaptureFunction); document.addEventListener("touchstart", () => setFocusVisible(false), true); const pointerCaptures = []; // PointerCaptureFunction is the global pointer handler we use to trap events and forward // them to the current capture if one exists. const pointerCaptureFunction = (event) => { // Track the position of the pointer as it moves. Pointer.position.x = event.clientX; Pointer.position.y = event.clientY; // Notify the pointer capture of the pointer movement and pointerup if one is signed up. for (let i = pointerCaptures.length - 1; i >= 0; i--) { const pointerCapture = pointerCaptures[i]; if (pointerCapture && pointerCapture.callback) { pointerCapture.callback(event); if (event.type === "pointerup") { Pointer.releaseCapture(pointerCapture.callback); } } } }; export const Pointer = { position: { x: 0, y: 0 }, releaseCapture: function releaseCapture(callback) { const pointerCaptureIndex = pointerCaptures.findIndex(pointerCapture => pointerCapture.callback === callback); if (pointerCaptureIndex > -1) { pointerCaptures.splice(pointerCaptureIndex, 1); } }, setCapture: function setCapture(callback) { // Update the pointerCapture to the new capture. pointerCaptures.push({ callback }); } }; document.addEventListener("pointermove", pointerCaptureFunction); document.addEventListener("pointerup", pointerCaptureFunction); document.addEventListener("pointerdown", event => { // Screen readers on scan mode trigger some key strokes as Pointer events. // We can easily identify those events because they have no coordinates. if (event.button === 0 && event.clientX === 0 && event.clientY === 0 && event.screenX === 0 && event.screenY === 0 && event.pageX === 0 && event.pageY === 0) { return; } setFocusVisible(false); }, true); /** * Returns the coordinates of a native event. For mouse / touch events, uses the * Mouse/Touch helpers. For a keyboard event, will return undefined. * @param event */ export function getPointByEventType(event) { if (event.clientX !== undefined) { return { x: Pointer.position.x, y: Pointer.position.y }; } else if (event.changedTouches || event.touches) { // If the event has a changedTouches or touches property, it is a touch event. return { x: Touch.position.x, y: Touch.position.y }; } else if (event.clientX !== undefined) { // If the event has a clientX, it is not a keyboard event, so treat it as a mouse event. return { x: Mouse.position.x, y: Mouse.position.y }; } return undefined; } /** * Checks two arrays to see they contain equal elements in the same order. * * @param array1 First array to check. * @param array2 Second array to check. * @param comparer Optional comparer to check whether array items are equal. If not specified, items are compared using strict equals. * @returns {boolean} */ export function arrayEquals(array1, array2, comparer = (item1, item2) => item1 === item2) { if (!array1 && !array2) { return true; } if (!array1 || !array2) { return false; } if (array1.length !== array2.length) { return false; } for (let i = 0; i < array1.length; i++) { if (!comparer(array1[i], array2[i])) { return false; } } return true; } export function isSafari() { const safari = /Safari\/([\d.]+)/i.exec(window.navigator.userAgent); return !!safari && navigator.userAgent.toLowerCase().includes("chrome"); } export function convertSpecialSymbols(value) { return value === null || value === void 0 ? void 0 : value.replace(/=/g, "eq").replace(/>/g, "gt").replace(/</g, "lt"); } /** * Returns a safe id with converted "=", ">", and "<" symbols. * * @param id The root id that is being made "Safe". */ export function getSafeIdWithSymbolConversion(id) { return getSafeId(convertSpecialSymbols(id)); }