UNPKG

azure-devops-ui

Version:

React components for building web UI in Azure DevOps

143 lines (142 loc) 6.13 kB
import "../../CommonImports"; import "../../Core/core.css"; import * as React from "react"; const FocusWithinContext = React.createContext({}); export class FocusWithin extends React.Component { constructor() { super(...arguments); this.blurTimeout = -1; this.focusCount = 0; this.focus = false; /** * onBlur method that should be attached to the onBlur handler of the * continers root element. */ this.onBlur = () => { // Don't let the focus count go below 0. // We have seen cases where we get a blur event, even when we // do not have focus. this.focusCount = Math.max(0, this.focusCount - 1); // Clear any previous timeout if we somehow got a second blur event before // ever processing the timeout from the first one. if (this.blurTimeout !== -1) { window.clearTimeout(this.blurTimeout); } // We must delay the blur processing for two basic reasons: // 1) If focus is transitioning to a child element we will fire a Blur // followed quickly by a Focus even though focus never left the element. // This causes problems for things like menus that close on loss of focus. // 2) IE 11 fires the blur before the focus (no other browser does this) // and this causes the same issue above but also causes focusCount // inconsistencies. this.blurTimeout = window.setTimeout(() => { this.blurTimeout = -1; if (!this.focusCount) { this.focus = false; // If we are tracking the focus state we will force a component update. if (this.props.updateStateOnFocusChange) { this.forceUpdate(); } if (this.props.onBlur) { this.props.onBlur(); } } }, 0); }; /** * onFocus method that should be attached to the onFocus handler of the * continer's root element. */ this.onFocus = (event) => { this.focusCount++; // If focus is just entering one of the child components and not just moving // one child to another we will call the onFocus delegate if supplied. if (!this.focus) { this.focus = true; // If we are tracking the focus state we will force a component update. if (this.props.updateStateOnFocusChange) { this.forceUpdate(); } if (this.props.onFocus) { this.props.onFocus(event); } } }; } render() { return (React.createElement(FocusWithinContext.Consumer, null, (focusWithinContext) => { let children; const newProps = { onBlur: this.onBlur, onFocus: this.onFocus }; // Save ou parent focus within for potential communication. this.parentFocusWithin = focusWithinContext.focusWithin; if (typeof this.props.children === "function") { const child = this.props.children; // For functional components we pass the hasFocus attribute as well. newProps.hasFocus = this.focus; children = child(newProps); } else { const child = React.Children.only(this.props.children); children = React.cloneElement(child, Object.assign(Object.assign({}, child.props), newProps), child.props.children); } return React.createElement(FocusWithinContext.Provider, { value: { focusWithin: this } }, children); })); } /** * componentWillUnmount is used to cleanup the component state. * * @NOTE: The main thing we need to deal with is when this component is unmounted * while it has focus. We need to get this FocusWithin and all of its parents state * updated since focus will move directly to the body without a blur event. */ componentWillUnmount() { if (this.blurTimeout !== -1) { window.clearTimeout(this.blurTimeout); this.blurTimeout = -1; } if (this.focusCount > 0) { this.unmountWithFocus(false); } } /** * hasFocus returns true if the focus is contained within the focus component * hierarchy. This includes portals, the element may or may not * be a direct descendant of the focus component in the DOM structure. */ hasFocus() { return this.focusCount > 0; } /** * When the focusWithin unmounts we need to determine if we currently have focus. * If we do, focus will be moved silently to the body. We need to cleanup the * focusWithin's that are affected by this silent change. */ unmountWithFocus(fromParent) { if (this.focusCount > 0) { this.focusCount--; if (this.focusCount > 0) { // If we are tracking the focus state we will force a component update. if (fromParent) { this.focusCount = 0; this.focus = false; if (this.props.updateStateOnFocusChange) { this.forceUpdate(); } if (this.props.onBlur) { this.props.onBlur(); } } } // Notify the parent focus within that the mounted focus component is unmounting. if (this.parentFocusWithin) { this.parentFocusWithin.unmountWithFocus(true); } } } } FocusWithin.defaultProps = { updateStateOnFocusChange: true };