UNPKG

office-ui-fabric-react

Version:

Reusable React components for building experiences for Office 365.

548 lines • 27.4 kB
import * as tslib_1 from "tslib"; import * as React from 'react'; import { getLayerStyles } from './KeytipLayer.styles'; import { Keytip } from '../../Keytip'; import { Layer } from '../../Layer'; import { BaseComponent, classNamesFunction, getDocument, arraysEqual, warn } from '../../Utilities'; import { KeytipManager } from '../../utilities/keytips/KeytipManager'; import { KeytipTree } from './KeytipTree'; import { ktpTargetFromId, ktpTargetFromSequences, sequencesToID, mergeOverflows } from '../../utilities/keytips/KeytipUtils'; import { transitionKeysContain, KeytipTransitionModifier } from '../../utilities/keytips/IKeytipTransitionKey'; import { KeytipEvents, KTP_LAYER_ID, KTP_ARIA_SEPARATOR } from '../../utilities/keytips/KeytipConstants'; var isMac = typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Macintosh') >= 0; // Default sequence is Alt-Windows (Alt-Meta) in Windows, Option-Control (Alt-Control) in Mac var defaultStartSequence = { key: isMac ? 'Control' : 'Meta', modifierKeys: [KeytipTransitionModifier.alt] }; // Default exit sequence is the same as the start sequence var defaultExitSequence = defaultStartSequence; // Default return sequence is Escape var defaultReturnSequence = { key: 'Escape' }; var getClassNames = classNamesFunction(); /** * A layer that holds all keytip items * * @export * @class KeytipLayer * @extends {BaseComponent<IKeytipLayerProps>} */ var KeytipLayerBase = /** @class */ (function (_super) { tslib_1.__extends(KeytipLayerBase, _super); // tslint:disable-next-line:no-any function KeytipLayerBase(props, context) { var _this = _super.call(this, props, context) || this; _this._keytipManager = KeytipManager.getInstance(); _this._delayedKeytipQueue = []; _this._keyHandled = false; _this._onDismiss = function (ev) { // if we are in keytip mode, then exit keytip mode if (_this.state.inKeytipMode) { _this._exitKeytipMode(ev); } }; _this._onKeyDown = function (ev) { _this._keyHandled = false; // using key since which has been deprecated and key is now widely suporrted. // See: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/which var key = ev.key; switch (key) { case 'Alt': // ALT puts focus in the browser bar, so it should not be used as a key for keytips. // It can be used as a modifier break; case 'Tab': case 'Enter': case 'Spacebar': case ' ': case 'ArrowUp': case 'Up': case 'ArrowDown': case 'Down': case 'ArrowLeft': case 'Left': case 'ArrowRight': case 'Right': if (_this.state.inKeytipMode) { _this._keyHandled = true; _this._exitKeytipMode(ev); } break; default: // Special cases for browser-specific keys that are not at standard // (according to http://www.w3.org/TR/uievents-key/#keys-navigation) if (key === 'Esc') { // Edge: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/5290772/ key = 'Escape'; } else if (key === 'OS' || key === 'Win') { // Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1232918 // Edge: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8860571/ // and https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/16424492/ key = 'Meta'; } var transitionKey = { key: key }; transitionKey.modifierKeys = _this._getModifierKey(key, ev); _this.processTransitionInput(transitionKey, ev); break; } }; _this._onKeyPress = function (ev) { if (_this.state.inKeytipMode && !_this._keyHandled) { // Call processInput _this.processInput(ev.key.toLocaleLowerCase(), ev); ev.preventDefault(); ev.stopPropagation(); } }; _this._onKeytipAdded = function (eventArgs) { var keytipProps = eventArgs.keytip; var uniqueID = eventArgs.uniqueID; _this._keytipTree.addNode(keytipProps, uniqueID); _this._setKeytips(); // Add the keytip to the queue to show later if (_this._keytipTree.isCurrentKeytipParent(keytipProps)) { _this._addKeytipToQueue(sequencesToID(keytipProps.keySequences)); } if (_this._newCurrentKeytipSequences && arraysEqual(keytipProps.keySequences, _this._newCurrentKeytipSequences)) { _this._triggerKeytipImmediately(keytipProps); } if (_this._isCurrentKeytipAnAlias(keytipProps)) { var keytipSequence = keytipProps.keySequences; if (keytipProps.overflowSetSequence) { keytipSequence = mergeOverflows(keytipSequence, keytipProps.overflowSetSequence); } _this._keytipTree.currentKeytip = _this._keytipTree.getNode(sequencesToID(keytipSequence)); } }; _this._onKeytipUpdated = function (eventArgs) { var keytipProps = eventArgs.keytip; var uniqueID = eventArgs.uniqueID; _this._keytipTree.updateNode(keytipProps, uniqueID); _this._setKeytips(); }; _this._onKeytipRemoved = function (eventArgs) { var keytipProps = eventArgs.keytip; var uniqueID = eventArgs.uniqueID; // Remove keytip from the delayed queue _this._removeKeytipFromQueue(sequencesToID(keytipProps.keySequences)); // Remove the node from the Tree _this._keytipTree.removeNode(keytipProps, uniqueID); _this._setKeytips(); }; _this._onPersistedKeytipAdded = function (eventArgs) { var keytipProps = eventArgs.keytip; var uniqueID = eventArgs.uniqueID; _this._keytipTree.addNode(keytipProps, uniqueID, true); }; _this._onPersistedKeytipRemoved = function (eventArgs) { var keytipProps = eventArgs.keytip; var uniqueID = eventArgs.uniqueID; _this._keytipTree.removeNode(keytipProps, uniqueID); }; _this._onPersistedKeytipExecute = function (eventArgs) { _this._persistedKeytipExecute(eventArgs.overflowButtonSequences, eventArgs.keytipSequences); }; /** * Sets if we are in keytip mode. * Note, this sets both the state for the layer as well as * the value that the manager will expose externally. * @param inKeytipMode - Boolean so set whether we are in keytip mode or not */ _this._setInKeytipMode = function (inKeytipMode) { _this.setState({ inKeytipMode: inKeytipMode }); _this._keytipManager.inKeytipMode = inKeytipMode; }; /** * Emits a warning if duplicate keytips are found for the children of the current keytip */ _this._warnIfDuplicateKeytips = function () { var duplicateKeytips = _this._getDuplicateIds(_this._keytipTree.getChildren()); if (duplicateKeytips.length) { warn('Duplicate keytips found for ' + duplicateKeytips.join(', ')); } }; /** * Returns duplicates among keytip IDs * If the returned array is empty, no duplicates were found * * @param keytipIds - Array of keytip IDs to find duplicates for * @returns {string[]} - Array of duplicates that were found. If multiple duplicates were found it will only be added once to this array */ _this._getDuplicateIds = function (keytipIds) { var seenIds = {}; return keytipIds.filter(function (keytipId) { seenIds[keytipId] = seenIds[keytipId] ? seenIds[keytipId] + 1 : 1; // Only add the first duplicate keytip seen return seenIds[keytipId] === 2; }); }; var managerKeytips = _this._keytipManager.getKeytips().slice(); _this.state = { inKeytipMode: false, // Get the initial set of keytips keytips: managerKeytips, visibleKeytips: _this._getVisibleKeytips(managerKeytips) }; _this._keytipTree = new KeytipTree(); // Add regular and persisted keytips to the tree for (var _i = 0, _a = _this._keytipManager.keytips.concat(_this._keytipManager.persistedKeytips); _i < _a.length; _i++) { var uniqueKeytip = _a[_i]; _this._keytipTree.addNode(uniqueKeytip.keytip, uniqueKeytip.uniqueID); } _this._currentSequence = ''; // Add keytip listeners _this._events.on(_this._keytipManager, KeytipEvents.KEYTIP_ADDED, _this._onKeytipAdded); _this._events.on(_this._keytipManager, KeytipEvents.KEYTIP_UPDATED, _this._onKeytipUpdated); _this._events.on(_this._keytipManager, KeytipEvents.KEYTIP_REMOVED, _this._onKeytipRemoved); _this._events.on(_this._keytipManager, KeytipEvents.PERSISTED_KEYTIP_ADDED, _this._onPersistedKeytipAdded); _this._events.on(_this._keytipManager, KeytipEvents.PERSISTED_KEYTIP_REMOVED, _this._onPersistedKeytipRemoved); _this._events.on(_this._keytipManager, KeytipEvents.PERSISTED_KEYTIP_EXECUTE, _this._onPersistedKeytipExecute); return _this; } KeytipLayerBase.prototype.render = function () { var _this = this; var _a = this.props, content = _a.content, styles = _a.styles; var _b = this.state, keytips = _b.keytips, visibleKeytips = _b.visibleKeytips; this._classNames = getClassNames(styles, {}); return (React.createElement(Layer, { styles: getLayerStyles }, React.createElement("span", { id: KTP_LAYER_ID, className: this._classNames.innerContent }, "" + content + KTP_ARIA_SEPARATOR), keytips && keytips.map(function (keytipProps, index) { return (React.createElement("span", { key: index, id: sequencesToID(keytipProps.keySequences), className: _this._classNames.innerContent }, keytipProps.keySequences.join(KTP_ARIA_SEPARATOR))); }), visibleKeytips && visibleKeytips.map(function (visibleKeytipProps) { return React.createElement(Keytip, tslib_1.__assign({ key: sequencesToID(visibleKeytipProps.keySequences) }, visibleKeytipProps)); }))); }; KeytipLayerBase.prototype.componentDidMount = function () { // Add window listeners this._events.on(window, 'mouseup', this._onDismiss, true /* useCapture */); this._events.on(window, 'pointerup', this._onDismiss, true /* useCapture */); this._events.on(window, 'resize', this._onDismiss); this._events.on(window, 'keydown', this._onKeyDown, true /* useCapture */); this._events.on(window, 'keypress', this._onKeyPress, true /* useCapture */); this._events.on(window, 'scroll', this._onDismiss, true /* useCapture */); // Add keytip listeners this._events.on(this._keytipManager, KeytipEvents.ENTER_KEYTIP_MODE, this._enterKeytipMode); this._events.on(this._keytipManager, KeytipEvents.EXIT_KEYTIP_MODE, this._exitKeytipMode); }; KeytipLayerBase.prototype.componentWillUnmount = function () { // Remove window listeners this._events.off(window, 'mouseup', this._onDismiss, true /* useCapture */); this._events.off(window, 'pointerup', this._onDismiss, true /* useCapture */); this._events.off(window, 'resize', this._onDismiss); this._events.off(window, 'keydown', this._onKeyDown, true /* useCapture */); this._events.off(window, 'keypress', this._onKeyPress, true /* useCapture */); this._events.off(window, 'scroll', this._onDismiss, true /* useCapture */); // Remove keytip listeners this._events.off(this._keytipManager, KeytipEvents.KEYTIP_ADDED, this._onKeytipAdded); this._events.off(this._keytipManager, KeytipEvents.KEYTIP_UPDATED, this._onKeytipUpdated); this._events.off(this._keytipManager, KeytipEvents.KEYTIP_REMOVED, this._onKeytipRemoved); this._events.off(this._keytipManager, KeytipEvents.PERSISTED_KEYTIP_ADDED, this._onPersistedKeytipAdded); this._events.off(this._keytipManager, KeytipEvents.PERSISTED_KEYTIP_REMOVED, this._onPersistedKeytipRemoved); this._events.off(this._keytipManager, KeytipEvents.PERSISTED_KEYTIP_EXECUTE, this._onPersistedKeytipExecute); this._events.off(this._keytipManager, KeytipEvents.ENTER_KEYTIP_MODE, this._enterKeytipMode); this._events.off(this._keytipManager, KeytipEvents.EXIT_KEYTIP_MODE, this._exitKeytipMode); }; // The below public functions are only public for testing purposes // They are not intended to be used in app code by using a KeytipLayer reference KeytipLayerBase.prototype.getCurrentSequence = function () { return this._currentSequence; }; KeytipLayerBase.prototype.getKeytipTree = function () { return this._keytipTree; }; /** * Processes an IKeytipTransitionKey entered by the user * * @param transitionKey - IKeytipTransitionKey received by the layer to process */ KeytipLayerBase.prototype.processTransitionInput = function (transitionKey, ev) { var currKtp = this._keytipTree.currentKeytip; if (transitionKeysContain(this.props.keytipExitSequences, transitionKey) && currKtp) { // If key sequence is in 'exit sequences', exit keytip mode this._keyHandled = true; this._exitKeytipMode(ev); } else if (transitionKeysContain(this.props.keytipReturnSequences, transitionKey)) { // If key sequence is in return sequences, move currentKeytip to parent (or if currentKeytip is the root, exit) if (currKtp) { this._keyHandled = true; if (currKtp.id === this._keytipTree.root.id) { // We are at the root, exit keytip mode this._exitKeytipMode(ev); } else { // If this keytip has a onReturn prop, we execute the func. if (currKtp.onReturn) { currKtp.onReturn(this._getKtpExecuteTarget(currKtp), this._getKtpTarget(currKtp)); } // Reset currentSequence this._currentSequence = ''; // Return pointer to its parent this._keytipTree.currentKeytip = this._keytipTree.getNode(currKtp.parent); // Show children keytips of the new currentKeytip this.showKeytips(this._keytipTree.getChildren()); this._warnIfDuplicateKeytips(); } } } else if (transitionKeysContain(this.props.keytipStartSequences, transitionKey) && !currKtp) { // If key sequence is in 'entry sequences' and currentKeytip is null, we enter keytip mode this._keyHandled = true; this._enterKeytipMode(); this._warnIfDuplicateKeytips(); } }; /** * Processes inputs from the document listener and traverse the keytip tree * * @param key - Key pressed by the user */ KeytipLayerBase.prototype.processInput = function (key, ev) { // Concat the input key with the current sequence var currSequence = this._currentSequence + key; var currKtp = this._keytipTree.currentKeytip; // currentKeytip must be defined, otherwise we haven't entered keytip mode yet if (currKtp) { var node = this._keytipTree.getExactMatchedNode(currSequence, currKtp); if (node) { this._keytipTree.currentKeytip = currKtp = node; var currKtpChildren = this._keytipTree.getChildren(); // Execute this node's onExecute if defined if (currKtp.onExecute) { currKtp.onExecute(this._getKtpExecuteTarget(currKtp), this._getKtpTarget(currKtp)); // Reset currKtp, this might have changed from the onExecute currKtp = this._keytipTree.currentKeytip; } // To exit keytipMode after executing the keytip it must not have a menu or have dynamic children if (currKtpChildren.length === 0 && !(currKtp.hasDynamicChildren || currKtp.hasMenu)) { this._exitKeytipMode(ev); } else { // Show all children keytips this.showKeytips(currKtpChildren); this._warnIfDuplicateKeytips(); } // Clear currentSequence this._currentSequence = ''; return; } var partialNodes = this._keytipTree.getPartiallyMatchedNodes(currSequence, currKtp); if (partialNodes.length > 0) { // We found nodes that partially match the sequence, so we show only those // Omit showing persisted nodes here var ids = partialNodes .filter(function (partialNode) { return !partialNode.persisted; }) .map(function (partialNode) { return partialNode.id; }); this.showKeytips(ids); // Save currentSequence this._currentSequence = currSequence; } } }; /** * Show the given keytips and hide all others * * @param ids - Keytip IDs to show */ KeytipLayerBase.prototype.showKeytips = function (ids) { // Update the visible prop in the manager for (var _i = 0, _a = this._keytipManager.getKeytips(); _i < _a.length; _i++) { var keytip = _a[_i]; var keytipId = sequencesToID(keytip.keySequences); if (ids.indexOf(keytipId) >= 0) { keytip.visible = true; } else if (keytip.overflowSetSequence && ids.indexOf(sequencesToID(mergeOverflows(keytip.keySequences, keytip.overflowSetSequence))) >= 0) { // Check if the ID with the overflow is the keytip we're looking for keytip.visible = true; } else { keytip.visible = false; } } // Apply the manager changes to the Layer state this._setKeytips(); }; /** * Enters keytip mode for this layer */ KeytipLayerBase.prototype._enterKeytipMode = function () { if (this._keytipManager.shouldEnterKeytipMode) { this._keytipTree.currentKeytip = this._keytipTree.root; // Show children of root this.showKeytips(this._keytipTree.getChildren()); this._setInKeytipMode(true /* inKeytipMode */); if (this.props.onEnterKeytipMode) { this.props.onEnterKeytipMode(); } } }; /** * Exits keytip mode for this layer */ KeytipLayerBase.prototype._exitKeytipMode = function (ev) { this._keytipTree.currentKeytip = undefined; this._currentSequence = ''; // Hide all keytips this.showKeytips([]); // Reset the delayed keytips if any this._delayedQueueTimeout && this._async.clearTimeout(this._delayedQueueTimeout); this._delayedKeytipQueue = []; this._setInKeytipMode(false /* inKeytipMode */); if (this.props.onExitKeytipMode) { this.props.onExitKeytipMode(ev); } }; /** * Sets the keytips state property * * @param keytipProps - Keytips to set in this layer */ KeytipLayerBase.prototype._setKeytips = function (keytipProps) { if (keytipProps === void 0) { keytipProps = this._keytipManager.getKeytips(); } this.setState({ keytips: keytipProps, visibleKeytips: this._getVisibleKeytips(keytipProps) }); }; /** * Callback function to use for persisted keytips * * @param overflowButtonSequences - The overflow button sequence to execute * @param keytipSequences - The keytip that should become the 'currentKeytip' when it is registered */ KeytipLayerBase.prototype._persistedKeytipExecute = function (overflowButtonSequences, keytipSequences) { // Save newCurrentKeytip for later this._newCurrentKeytipSequences = keytipSequences; // Execute the overflow button's onExecute var overflowKeytipNode = this._keytipTree.getNode(sequencesToID(overflowButtonSequences)); if (overflowKeytipNode && overflowKeytipNode.onExecute) { overflowKeytipNode.onExecute(this._getKtpExecuteTarget(overflowKeytipNode), this._getKtpTarget(overflowKeytipNode)); } }; KeytipLayerBase.prototype._getVisibleKeytips = function (keytips) { // Filter out non-visible keytips and duplicates var seenIds = {}; return keytips.filter(function (keytip) { var keytipId = sequencesToID(keytip.keySequences); seenIds[keytipId] = seenIds[keytipId] ? seenIds[keytipId] + 1 : 1; return keytip.visible && seenIds[keytipId] === 1; }); }; /** * Gets the ModifierKeyCodes based on the keyboard event * * @param ev - React.KeyboardEvent * @returns List of ModifierKeyCodes that were pressed */ KeytipLayerBase.prototype._getModifierKey = function (key, ev) { var modifierKeys = []; if (ev.altKey && key !== 'Alt') { modifierKeys.push(KeytipTransitionModifier.alt); } if (ev.ctrlKey && key !== 'Control') { modifierKeys.push(KeytipTransitionModifier.ctrl); } if (ev.shiftKey && key !== 'Shift') { modifierKeys.push(KeytipTransitionModifier.shift); } if (ev.metaKey && key !== 'Meta') { modifierKeys.push(KeytipTransitionModifier.meta); } return modifierKeys.length ? modifierKeys : undefined; }; /** * Trigger a keytip immediately and set it as the current keytip * * @param keytipProps - Keytip to trigger immediately */ KeytipLayerBase.prototype._triggerKeytipImmediately = function (keytipProps) { // This keytip should become the currentKeytip and should execute right away var keytipSequence = keytipProps.keySequences.slice(); if (keytipProps.overflowSetSequence) { keytipSequence = mergeOverflows(keytipSequence, keytipProps.overflowSetSequence); } // Set currentKeytip this._keytipTree.currentKeytip = this._keytipTree.getNode(sequencesToID(keytipSequence)); if (this._keytipTree.currentKeytip) { // Show all children keytips if any var children = this._keytipTree.getChildren(); if (children.length) { this.showKeytips(children); } if (this._keytipTree.currentKeytip.onExecute) { this._keytipTree.currentKeytip.onExecute(this._getKtpExecuteTarget(this._keytipTree.currentKeytip), this._getKtpTarget(this._keytipTree.currentKeytip)); } } // Unset _newCurrKtpSequences this._newCurrentKeytipSequences = undefined; }; KeytipLayerBase.prototype._addKeytipToQueue = function (keytipID) { var _this = this; // Add keytip this._delayedKeytipQueue.push(keytipID); // Clear timeout this._delayedQueueTimeout && this._async.clearTimeout(this._delayedQueueTimeout); // Reset timeout this._delayedQueueTimeout = this._async.setTimeout(function () { if (_this._delayedKeytipQueue.length) { _this.showKeytips(_this._delayedKeytipQueue); _this._delayedKeytipQueue = []; } }, 300); }; KeytipLayerBase.prototype._removeKeytipFromQueue = function (keytipID) { var _this = this; var index = this._delayedKeytipQueue.indexOf(keytipID); if (index >= 0) { // Remove keytip this._delayedKeytipQueue.splice(index, 1); // Clear timeout this._delayedQueueTimeout && this._async.clearTimeout(this._delayedQueueTimeout); // Reset timeout this._delayedQueueTimeout = this._async.setTimeout(function () { if (_this._delayedKeytipQueue.length) { _this.showKeytips(_this._delayedKeytipQueue); _this._delayedKeytipQueue = []; } }, 300); } }; KeytipLayerBase.prototype._getKtpExecuteTarget = function (currKtp) { return getDocument().querySelector(ktpTargetFromId(currKtp.id)); }; KeytipLayerBase.prototype._getKtpTarget = function (currKtp) { return getDocument().querySelector(ktpTargetFromSequences(currKtp.keySequences)); }; /** * Returns T/F if the keytipProps keySequences match the currentKeytip, and the currentKeytip is in an overflow well * This will make 'keytipProps' the new currentKeytip * * @param keytipProps - Keytip props to check * @returns {boolean} - T/F if this keytip should become the currentKeytip */ KeytipLayerBase.prototype._isCurrentKeytipAnAlias = function (keytipProps) { var currKtp = this._keytipTree.currentKeytip; if (currKtp && (currKtp.overflowSetSequence || currKtp.persisted) && arraysEqual(keytipProps.keySequences, currKtp.keySequences)) { return true; } return false; }; KeytipLayerBase.defaultProps = { keytipStartSequences: [defaultStartSequence], keytipExitSequences: [defaultExitSequence], keytipReturnSequences: [defaultReturnSequence], content: '' }; return KeytipLayerBase; }(BaseComponent)); export { KeytipLayerBase }; //# sourceMappingURL=KeytipLayer.base.js.map