UNPKG

azure-devops-ui

Version:

React components for building web UI in Azure DevOps

313 lines (312 loc) 16.7 kB
import { __assign, __extends } from "tslib"; import "../../CommonImports"; import "../../Core/core.css"; import * as React from "react"; import { FocusGroup } from '../../FocusGroup'; import { ElementRelationship, getRelationship, KeyCode, shimRef } from '../../Util'; import { FocusZoneDirection, FocusZoneKeyStroke } from "./FocusZone.Props"; // The FocusZoneContext carries the identifier for the current FocusZone. export var FocusZoneContext = React.createContext({ direction: undefined, focuszoneId: undefined }); // As an event propagates through the hierarchy of focus zones it may // be marked as ignored. This allows a child focus zone to mark an event // as "pass-through" for all of its parents. var ignoreEvent = false; // An internal identifier used to created unique focuszoneId's. var focuszoneId = 1; var FocusZone = /** @class */ (function (_super) { __extends(FocusZone, _super); function FocusZone(props) { var _this = _super.call(this, props) || this; _this.rootElements = []; _this.state = { focuszoneId: "focuszone-" + focuszoneId++ }; return _this; } FocusZone.prototype.render = function () { var _this = this; // We need to shim the KeyDown event on each of the children. This allows us to capture // the event and process it for focus changes. var content = (React.createElement(FocusZoneContext.Consumer, null, function (parentContext) { return (React.createElement(FocusZoneContext.Provider, { value: { direction: _this.props.direction, focuszoneId: _this.state.focuszoneId } }, React.Children.map(_this.props.children, function (child, index) { if (child === null || typeof child === "string" || typeof child === "number") { return child; } // All direct children MUST be DOM elements. if (typeof child.type !== "string") { throw Error("Children of a focus zone MUST be DOM elements"); } // Save the supplied keydown event handler so we can forward the event to it. var existingOnKeyDown = child.props.onKeyDown; var existingOnFocus = child.props.onFocus; // Save the component reference for this element, either the one from the original // component or the one we added. _this.rootElements[index] = shimRef(child); return React.cloneElement(child, __assign(__assign({ key: index }, child.props), { ref: _this.rootElements[index], onFocus: function (event) { var _a; if (existingOnFocus) { existingOnFocus(event); } var focusCurrent = document.activeElement; for (var index_1 = 0; index_1 < _this.rootElements.length; index_1++) { var rootElement = (_a = _this.rootElements[index_1]) === null || _a === void 0 ? void 0 : _a.current; if (rootElement && (rootElement.contains(focusCurrent) || rootElement === focusCurrent)) { _this.lastFocusElement = event.target; } } }, onKeyDown: function (event) { var ignoreKeystroke = FocusZoneKeyStroke.IgnoreNone; if (existingOnKeyDown) { existingOnKeyDown(event); } // Determine whether or not this focuszone wants to preprocess this keystroke // and mark the current propagation as ignored. if (!ignoreEvent && _this.props.preprocessKeyStroke) { ignoreKeystroke = _this.props.preprocessKeyStroke(event); if (ignoreKeystroke === FocusZoneKeyStroke.IgnoreAll) { ignoreEvent = true; } } if (!ignoreEvent) { if (!event.defaultPrevented && !_this.props.disabled) { var nodeName = event.target.nodeName; var offset = void 0; // Logic to handle input / text area tags var inputPosition = void 0; var inputLength = void 0; if (nodeName === "INPUT" || nodeName === "TEXTAREA") { var input = event.target; try { inputPosition = typeof input.selectionStart === "number" ? input.selectionStart : undefined; } catch (_a) { // Microsoft Edge throws InvalidStateError when calling 'input.selectionStart' on non-supported input element types // according to https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement // Ignore this error } inputLength = input.value.length; } var allowLeftArrow = inputPosition === undefined || (inputPosition === 0 && _this.props.allowArrowOutOfInputs); var allowRightArrow = inputPosition === undefined || inputLength === undefined || (inputPosition === inputLength && _this.props.allowArrowOutOfInputs); switch (event.which) { case KeyCode.upArrow: if (nodeName !== "TEXTAREA") { if (_this.props.direction === FocusZoneDirection.Vertical) { offset = -1; } } break; case KeyCode.downArrow: if (nodeName !== "TEXTAREA") { if (_this.props.direction === FocusZoneDirection.Vertical) { offset = 1; } } break; case KeyCode.rightArrow: if (allowRightArrow) { if (_this.props.direction === FocusZoneDirection.Horizontal) { offset = 1; } } break; case KeyCode.leftArrow: if (allowLeftArrow) { if (_this.props.direction === FocusZoneDirection.Horizontal) { offset = -1; } } break; case KeyCode.tab: if (_this.props.handleTabKey) { offset = event.shiftKey ? -1 : 1; } break; case KeyCode.enter: if (_this.props.activateOnEnter) { event.target.click(); } } if (offset) { if (_this.focusNextElement(event, offset)) { event.preventDefault(); } } } } if (ignoreKeystroke === FocusZoneKeyStroke.IgnoreParents) { ignoreEvent = true; } // Perform any supplied event post processing. if (!ignoreEvent && _this.props.postprocessKeyStroke) { if (_this.props.postprocessKeyStroke(event) === FocusZoneKeyStroke.IgnoreParents) { ignoreEvent = true; } } // Once we reach the root focuszone we need to clear the ignoredEvent. if (!parentContext.focuszoneId) { ignoreEvent = false; } } })); }))); })); if (this.props.focusGroupProps) { content = React.createElement(FocusGroup, __assign({}, this.props.focusGroupProps), content); } return content; }; FocusZone.prototype.componentDidMount = function () { var focusElement; // If a defaultActiveElement is supplied we will focus it. It is not required to // be member of the focus zone, it can be any element. if (this.props.focusOnMount) { var defaultActiveElement = this.props.defaultActiveElement; var focusElements = this.getFocusElements(typeof defaultActiveElement === "function" ? defaultActiveElement() : defaultActiveElement); if (focusElements.length > 0) { focusElement = focusElements[0]; } } if (focusElement) { focusElement.focus({ preventScroll: this.props.preventScrollOnFocus }); } }; FocusZone.prototype.focusNextElement = function (event, offset) { var focusElements = this.getFocusElements(); if (focusElements.length > 0) { var focusCurrent = document.activeElement; var rootElements = this.rootElements; // Determine if an element in the focus zone has focus. var focusIndex = focusElements.indexOf(focusCurrent); // Focus may not be on an element in the zone so we need to // figure out which one we are between in this case. if (focusIndex === -1) { var index = 0; // Determine if the element is in a portal or directly within a focuszone root. for (index = 0; index < rootElements.length; index++) { var elementRef = rootElements[index]; if (elementRef.current) { if (elementRef.current.contains(event.target)) { break; } } } // If this is coming from a portal, we will use the element that last had focus. if (index === this.rootElements.length && this.lastFocusElement) { focusIndex = focusElements.indexOf(this.lastFocusElement); } else { for (index = 0; index < focusElements.length; index++) { var relationship = getRelationship(focusCurrent, focusElements[index]); if (relationship === ElementRelationship.Before) { focusIndex = index - (offset > 0 ? 1 : 0); break; } else if (relationship === ElementRelationship.Child) { focusIndex = index; break; } else if (relationship === ElementRelationship.After && index === focusElements.length - 1) { focusIndex = focusElements.length; } } } } // Move to the next component in the set of focus zone components. focusIndex += offset; // If the FocusZone supports circular navigation and we are on the end // we will move to the element on the opposite end. if (this.props.circularNavigation) { if (focusIndex < 0) { focusIndex = focusElements.length - 1; } else if (focusIndex >= focusElements.length) { focusIndex = 0; } } // If we ended up on a focusable element update the focus. if (focusIndex > -1 && focusIndex < focusElements.length) { focusElements[focusIndex].focus(); if (this.props.selectInputTextOnFocus && focusElements[focusIndex] instanceof HTMLInputElement) { focusElements[focusIndex].select(); } return true; } } return false; }; FocusZone.prototype.getFocusElements = function (customSelector) { var focusElements = []; var selector = customSelector; // If a custom selector was supplied we will use it. if (!selector) { // The default selector will just pick up items tagged with this focuszone id. selector = "[data-focuszone~=" + this.state.focuszoneId + "]"; // If we are including the default elements from the DOM we will add the // default selector to our list of selectors. if (this.props.includeDefaults) { selector += ",a[href],button,iframe,input,select,textarea,[tabIndex]"; } } // Filter the elements that matched our query to the elements that are elligible // for receiving focus in this focuszone. for (var _i = 0, _a = this.rootElements; _i < _a.length; _i++) { var rootElement = _a[_i]; if (rootElement.current) { var focusChildren = rootElement.current.querySelectorAll(selector); // Check if the root element matches our selector. if (rootElement.current.matches(selector) && this.isFocusElement(rootElement.current, customSelector)) { focusElements.push(rootElement.current); } // Check all the children of the root that are potential focus elements. for (var rootIndex = 0; rootIndex < focusChildren.length; rootIndex++) { var element = focusChildren[rootIndex]; if (this.isFocusElement(element, customSelector)) { focusElements.push(element); } } } } return focusElements; }; /** * isFocusElement is used to determine whether or not an element should participate * in this focus zone. * * @param element HTML Element that you are testing as a valid focus element. * * @param customSelector A custom selector that is used to match elements with * negative tabIndex. These wont match normally unless targetted by the custom * selector. */ FocusZone.prototype.isFocusElement = function (element, customSelector) { // Filter out elements that are disabled. if (element.hasAttribute("disabled")) { return false; } if (!customSelector) { // Filter out elements that are not visible. if (!this.props.skipHiddenCheck) { var style = window.getComputedStyle(element); if (style.visibility === "hidden" || style.display === "none" || !(element.offsetWidth || element.offsetHeight || element.getClientRects().length)) { return false; } } // Filter out elements with negative tabIndex that aren't // explicity marked for this focuszone. var tabIndex = element.getAttribute("tabindex"); if (tabIndex && parseInt(tabIndex) < 0) { var focuszoneId_1 = element.getAttribute("data-focuszone"); if (!focuszoneId_1 || focuszoneId_1.indexOf(this.state.focuszoneId) < 0) { return false; } } } return true; }; return FocusZone; }(React.Component)); export { FocusZone };