UNPKG

balm-ui

Version:

A modular and customizable UI library based on Material Design and Vue 3

977 lines 69.2 kB
/** * @license * Copyright 2020 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import { __assign, __extends, __values } from "tslib"; import { AnimationFrame } from '../animation/animationframe'; import { getCorrectPropertyName } from '../animation/util'; import { MDCFoundation } from '../base/foundation'; import { KEY, normalizeKey } from '../dom/keyboard'; import { AnchorBoundaryType, attributes, CssClasses, numbers, PositionWithCaret, strings, XPosition, XPositionWithCaret, YPosition, YPositionWithCaret } from './constants'; var RICH = CssClasses.RICH, SHOWN = CssClasses.SHOWN, SHOWING = CssClasses.SHOWING, SHOWING_TRANSITION = CssClasses.SHOWING_TRANSITION, HIDE = CssClasses.HIDE, HIDE_TRANSITION = CssClasses.HIDE_TRANSITION, MULTILINE_TOOLTIP = CssClasses.MULTILINE_TOOLTIP; var AnimationKeys; (function (AnimationKeys) { AnimationKeys["POLL_ANCHOR"] = "poll_anchor"; })(AnimationKeys || (AnimationKeys = {})); // Accessing `window` without a `typeof` check will throw on Node environments. var HAS_WINDOW = typeof window !== 'undefined'; var MDCTooltipFoundation = /** @class */ (function (_super) { __extends(MDCTooltipFoundation, _super); function MDCTooltipFoundation(adapter) { var _this = _super.call(this, __assign(__assign({}, MDCTooltipFoundation.defaultAdapter), adapter)) || this; _this.tooltipShown = false; _this.anchorGap = numbers.BOUNDED_ANCHOR_GAP; _this.xTooltipPos = XPosition.DETECTED; _this.yTooltipPos = YPosition.DETECTED; _this.tooltipPositionWithCaret = PositionWithCaret.DETECTED; // Minimum threshold distance needed between the tooltip and the viewport. _this.minViewportTooltipThreshold = numbers.MIN_VIEWPORT_TOOLTIP_THRESHOLD; _this.hideDelayMs = numbers.HIDE_DELAY_MS; _this.showDelayMs = numbers.SHOW_DELAY_MS; _this.anchorRect = null; _this.parentRect = null; _this.frameId = null; _this.hideTimeout = null; _this.showTimeout = null; _this.addAncestorScrollEventListeners = new Array(); _this.removeAncestorScrollEventListeners = new Array(); _this.animFrame = new AnimationFrame(); _this.anchorBlurHandler = function (evt) { _this.handleAnchorBlur(evt); }; _this.documentClickHandler = function (evt) { _this.handleDocumentClick(evt); }; _this.documentKeydownHandler = function (evt) { _this.handleKeydown(evt); }; _this.tooltipMouseEnterHandler = function () { _this.handleTooltipMouseEnter(); }; _this.tooltipMouseLeaveHandler = function () { _this.handleTooltipMouseLeave(); }; _this.richTooltipFocusOutHandler = function (evt) { _this.handleRichTooltipFocusOut(evt); }; _this.windowScrollHandler = function () { _this.handleWindowScrollEvent(); }; _this.windowResizeHandler = function () { _this.handleWindowChangeEvent(); }; return _this; } Object.defineProperty(MDCTooltipFoundation, "defaultAdapter", { get: function () { return { getAttribute: function () { return null; }, setAttribute: function () { return undefined; }, removeAttribute: function () { return undefined; }, addClass: function () { return undefined; }, hasClass: function () { return false; }, removeClass: function () { return undefined; }, getComputedStyleProperty: function () { return ''; }, setStyleProperty: function () { return undefined; }, setSurfaceAnimationStyleProperty: function () { return undefined; }, getViewportWidth: function () { return 0; }, getViewportHeight: function () { return 0; }, getTooltipSize: function () { return ({ width: 0, height: 0 }); }, getAnchorBoundingRect: function () { return ({ top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0 }); }, getParentBoundingRect: function () { return ({ top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0 }); }, getAnchorAttribute: function () { return null; }, setAnchorAttribute: function () { return null; }, isRTL: function () { return false; }, anchorContainsElement: function () { return false; }, tooltipContainsElement: function () { return false; }, focusAnchorElement: function () { return undefined; }, registerEventHandler: function () { return undefined; }, deregisterEventHandler: function () { return undefined; }, registerAnchorEventHandler: function () { return undefined; }, deregisterAnchorEventHandler: function () { return undefined; }, registerDocumentEventHandler: function () { return undefined; }, deregisterDocumentEventHandler: function () { return undefined; }, registerWindowEventHandler: function () { return undefined; }, deregisterWindowEventHandler: function () { return undefined; }, notifyHidden: function () { return undefined; }, getTooltipCaretBoundingRect: function () { return ({ top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0 }); }, setTooltipCaretStyle: function () { return undefined; }, clearTooltipCaretStyles: function () { return undefined; }, getActiveElement: function () { return null; }, }; }, enumerable: false, configurable: true }); MDCTooltipFoundation.prototype.init = function () { this.richTooltip = this.adapter.hasClass(RICH); this.persistentTooltip = this.adapter.getAttribute(attributes.PERSISTENT) === 'true'; this.interactiveTooltip = !!this.adapter.getAnchorAttribute(attributes.ARIA_EXPANDED) && this.adapter.getAnchorAttribute(attributes.ARIA_HASPOPUP) === 'dialog'; this.hasCaret = this.richTooltip && this.adapter.getAttribute(attributes.HAS_CARET) === 'true'; }; MDCTooltipFoundation.prototype.isShown = function () { return this.tooltipShown; }; MDCTooltipFoundation.prototype.isRich = function () { return this.richTooltip; }; MDCTooltipFoundation.prototype.isPersistent = function () { return this.persistentTooltip; }; MDCTooltipFoundation.prototype.handleAnchorMouseEnter = function () { var _this = this; if (this.tooltipShown) { // Covers the instance where a user hovers over the anchor to reveal the // tooltip, and then quickly navigates away and then back to the anchor. // The tooltip should stay visible without animating out and then back in // again. this.show(); } else { // clearHideTimeout here since handleAnchorMouseLeave sets a hideTimeout // and that can execute before the showTimeout executes, resulting in hide // being called and the showTimeout set below to be cleared. this.clearHideTimeout(); this.showTimeout = setTimeout(function () { _this.show(); }, this.showDelayMs); } }; MDCTooltipFoundation.prototype.handleAnchorTouchstart = function () { var _this = this; this.showTimeout = setTimeout(function () { _this.show(); }, this.showDelayMs); // Prevent a context menu from appearing if user is long-pressing on a // tooltip anchor. this.adapter.registerWindowEventHandler('contextmenu', this.preventContextMenuOnLongTouch); }; MDCTooltipFoundation.prototype.preventContextMenuOnLongTouch = function (evt) { evt.preventDefault(); }; MDCTooltipFoundation.prototype.handleAnchorTouchend = function () { this.clearShowTimeout(); // Only remove the 'contextmenu' listener if the tooltip is not shown. When // the tooltip *is* shown, listener is removed in the close method. if (!this.isShown()) { this.adapter.deregisterWindowEventHandler('contextmenu', this.preventContextMenuOnLongTouch); } }; MDCTooltipFoundation.prototype.handleAnchorFocus = function (evt) { var _this = this; // TODO(b/157075286): Need to add some way to distinguish keyboard // navigation focus events from other focus events, and only show the // tooltip on the former of these events. var relatedTarget = evt.relatedTarget; var tooltipContainsRelatedTarget = relatedTarget instanceof HTMLElement && this.adapter.tooltipContainsElement(relatedTarget); // Do not show tooltip if the previous focus was on a tooltip element. This // occurs when a rich tooltip is closed and focus is restored to the anchor // or when user tab-navigates back into the anchor from the rich tooltip. if (tooltipContainsRelatedTarget) { return; } this.showTimeout = setTimeout(function () { _this.show(); }, this.showDelayMs); }; MDCTooltipFoundation.prototype.handleAnchorMouseLeave = function () { var _this = this; this.clearShowTimeout(); this.hideTimeout = setTimeout(function () { _this.hide(); }, this.hideDelayMs); }; MDCTooltipFoundation.prototype.handleAnchorClick = function () { if (this.tooltipShown) { this.hide(); } else { this.show(); } }; MDCTooltipFoundation.prototype.handleDocumentClick = function (evt) { var anchorOrTooltipContainsTargetElement = evt.target instanceof HTMLElement && (this.adapter.anchorContainsElement(evt.target) || this.adapter.tooltipContainsElement(evt.target)); // For persistent rich tooltips, we will not hide if: // - The click target is within the anchor element. Otherwise, both // the anchor element's click handler and this handler will handle the // click (due to event propagation), resulting in a shown tooltip // being immediately hidden if the tooltip was initially hidden. // - The click target is within the tooltip element, since clicks // on the tooltip do not close the tooltip. if (this.richTooltip && this.persistentTooltip && anchorOrTooltipContainsTargetElement) { return; } // Hide the tooltip immediately on click. this.hide(); }; MDCTooltipFoundation.prototype.handleKeydown = function (evt) { // Hide the tooltip immediately on ESC key. var key = normalizeKey(evt); if (key === KEY.ESCAPE) { var activeElement = this.adapter.getActiveElement(); var tooltipContainsActiveElement = activeElement instanceof HTMLElement && this.adapter.tooltipContainsElement(activeElement); if (tooltipContainsActiveElement) { this.adapter.focusAnchorElement(); } this.hide(); } }; MDCTooltipFoundation.prototype.handleAnchorBlur = function (evt) { if (this.richTooltip) { var tooltipContainsRelatedTargetElement = evt.relatedTarget instanceof HTMLElement && this.adapter.tooltipContainsElement(evt.relatedTarget); // If focus changed to the tooltip element, don't hide the tooltip. if (tooltipContainsRelatedTargetElement) { return; } if (evt.relatedTarget === null && this.interactiveTooltip) { // If evt.relatedTarget is null, it is because focus is moving to an // element that is not focusable. This should only occur in instances // of a screen reader in browse mode/linear navigation mode. If the // tooltip is interactive (and so the entire content is not read by // the screen reader upon the tooltip being opened), we want to allow // users to read the content of the tooltip (and not just the focusable // elements). return; } } // Hide tooltip immediately on focus change. this.hide(); }; MDCTooltipFoundation.prototype.handleTooltipMouseEnter = function () { this.show(); }; MDCTooltipFoundation.prototype.handleTooltipMouseLeave = function () { var _this = this; this.clearShowTimeout(); this.hideTimeout = setTimeout(function () { _this.hide(); }, this.hideDelayMs); }; MDCTooltipFoundation.prototype.handleRichTooltipFocusOut = function (evt) { var anchorOrTooltipContainsRelatedTargetElement = evt.relatedTarget instanceof HTMLElement && (this.adapter.anchorContainsElement(evt.relatedTarget) || this.adapter.tooltipContainsElement(evt.relatedTarget)); // If the focus is still within the anchor or the tooltip, do not hide the // tooltip. if (anchorOrTooltipContainsRelatedTargetElement) { return; } if (evt.relatedTarget === null && this.interactiveTooltip) { // If evt.relatedTarget is null, it is because focus is moving to an // element that is not focusable. This should only occur in instances // of a screen reader in browse mode/linear navigation mode. If the // tooltip is interactive (and so the entire content is not read by // the screen reader upon the tooltip being opened), we want to allow // users to read the content of the tooltip (and not just the focusable // elements). return; } this.hide(); }; MDCTooltipFoundation.prototype.handleWindowScrollEvent = function () { if (this.persistentTooltip) { // Persistent tooltips remain visible on user scroll, call appropriate // handler to ensure the tooltip remains pinned to the anchor on page // scroll. this.handleWindowChangeEvent(); return; } this.hide(); }; /** * On window resize or scroll, check the anchor position and size and * repostion tooltip if necessary. */ MDCTooltipFoundation.prototype.handleWindowChangeEvent = function () { var _this = this; // Since scroll and resize events can fire at a high rate, we throttle // the potential re-positioning of tooltip component using // requestAnimationFrame. this.animFrame.request(AnimationKeys.POLL_ANCHOR, function () { _this.repositionTooltipOnAnchorMove(); }); }; MDCTooltipFoundation.prototype.show = function () { var e_1, _a; var _this = this; this.clearHideTimeout(); this.clearShowTimeout(); if (this.tooltipShown) { return; } this.tooltipShown = true; this.adapter.removeAttribute('aria-hidden'); if (this.richTooltip) { if (this.interactiveTooltip) { this.adapter.setAnchorAttribute('aria-expanded', 'true'); } this.adapter.registerEventHandler('focusout', this.richTooltipFocusOutHandler); } if (!this.persistentTooltip) { this.adapter.registerEventHandler('mouseenter', this.tooltipMouseEnterHandler); this.adapter.registerEventHandler('mouseleave', this.tooltipMouseLeaveHandler); } this.adapter.removeClass(HIDE); this.adapter.addClass(SHOWING); if (this.isTooltipMultiline() && !this.richTooltip) { this.adapter.addClass(MULTILINE_TOOLTIP); } this.anchorRect = this.adapter.getAnchorBoundingRect(); this.parentRect = this.adapter.getParentBoundingRect(); this.richTooltip ? this.positionRichTooltip() : this.positionPlainTooltip(); this.adapter.registerAnchorEventHandler('blur', this.anchorBlurHandler); this.adapter.registerDocumentEventHandler('click', this.documentClickHandler); this.adapter.registerDocumentEventHandler('keydown', this.documentKeydownHandler); this.adapter.registerWindowEventHandler('scroll', this.windowScrollHandler); this.adapter.registerWindowEventHandler('resize', this.windowResizeHandler); try { // Register any additional scroll handlers for (var _b = __values(this.addAncestorScrollEventListeners), _c = _b.next(); !_c.done; _c = _b.next()) { var fn = _c.value; fn(); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_1) throw e_1.error; } } this.frameId = requestAnimationFrame(function () { _this.clearAllAnimationClasses(); _this.adapter.addClass(SHOWN); _this.adapter.addClass(SHOWING_TRANSITION); }); }; MDCTooltipFoundation.prototype.hide = function () { var e_2, _a; this.clearHideTimeout(); this.clearShowTimeout(); if (!this.tooltipShown) { return; } if (this.frameId) { cancelAnimationFrame(this.frameId); } this.tooltipShown = false; this.adapter.setAttribute('aria-hidden', 'true'); this.adapter.deregisterEventHandler('focusout', this.richTooltipFocusOutHandler); if (this.richTooltip) { if (this.interactiveTooltip) { this.adapter.setAnchorAttribute('aria-expanded', 'false'); } } if (!this.persistentTooltip) { this.adapter.deregisterEventHandler('mouseenter', this.tooltipMouseEnterHandler); this.adapter.deregisterEventHandler('mouseleave', this.tooltipMouseLeaveHandler); } this.clearAllAnimationClasses(); this.adapter.addClass(HIDE); this.adapter.addClass(HIDE_TRANSITION); this.adapter.removeClass(SHOWN); this.adapter.deregisterAnchorEventHandler('blur', this.anchorBlurHandler); this.adapter.deregisterDocumentEventHandler('click', this.documentClickHandler); this.adapter.deregisterDocumentEventHandler('keydown', this.documentKeydownHandler); this.adapter.deregisterWindowEventHandler('scroll', this.windowScrollHandler); this.adapter.deregisterWindowEventHandler('resize', this.windowResizeHandler); this.adapter.deregisterWindowEventHandler('contextmenu', this.preventContextMenuOnLongTouch); try { // Deregister any additional scroll handlers for (var _b = __values(this.removeAncestorScrollEventListeners), _c = _b.next(); !_c.done; _c = _b.next()) { var fn = _c.value; fn(); } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_2) throw e_2.error; } } }; MDCTooltipFoundation.prototype.handleTransitionEnd = function () { var isHidingTooltip = this.adapter.hasClass(HIDE); this.adapter.removeClass(SHOWING); this.adapter.removeClass(SHOWING_TRANSITION); this.adapter.removeClass(HIDE); this.adapter.removeClass(HIDE_TRANSITION); // If handleTransitionEnd is called after hiding the tooltip, the tooltip // will have the HIDE class (before calling the adapter removeClass method). // If tooltip is now hidden, send a notification that the animation has // completed and the tooltip is no longer visible. // We don't send a notification of the animation completing if a showTimeout // value is set -- this happens when a user triggers a tooltip to be shown // while that tooltip is fading. Once this hide transition is completed, // that same tooltip will be re-shown. if (isHidingTooltip && this.showTimeout === null) { this.adapter.notifyHidden(); } }; MDCTooltipFoundation.prototype.clearAllAnimationClasses = function () { this.adapter.removeClass(SHOWING_TRANSITION); this.adapter.removeClass(HIDE_TRANSITION); }; MDCTooltipFoundation.prototype.setTooltipPosition = function (position) { var xPos = position.xPos, yPos = position.yPos, withCaretPos = position.withCaretPos; if (this.hasCaret && withCaretPos) { this.tooltipPositionWithCaret = withCaretPos; return; } if (xPos) { this.xTooltipPos = xPos; } if (yPos) { this.yTooltipPos = yPos; } }; MDCTooltipFoundation.prototype.setAnchorBoundaryType = function (type) { if (type === AnchorBoundaryType.UNBOUNDED) { this.anchorGap = numbers.UNBOUNDED_ANCHOR_GAP; } else { this.anchorGap = numbers.BOUNDED_ANCHOR_GAP; } }; MDCTooltipFoundation.prototype.setShowDelay = function (delayMs) { this.showDelayMs = delayMs; }; MDCTooltipFoundation.prototype.setHideDelay = function (delayMs) { this.hideDelayMs = delayMs; }; MDCTooltipFoundation.prototype.isTooltipMultiline = function () { var tooltipSize = this.adapter.getTooltipSize(); return tooltipSize.height > numbers.MIN_HEIGHT && tooltipSize.width >= numbers.MAX_WIDTH; }; MDCTooltipFoundation.prototype.positionPlainTooltip = function () { // A plain tooltip has `fixed` positioning and is placed as an immediate // child of the document body. Its positioning is calculated with respect to // the viewport. var _a = this.calculateTooltipStyles(this.anchorRect), top = _a.top, yTransformOrigin = _a.yTransformOrigin, left = _a.left, xTransformOrigin = _a.xTransformOrigin; var transformProperty = HAS_WINDOW ? getCorrectPropertyName(window, 'transform') : 'transform'; this.adapter.setSurfaceAnimationStyleProperty(transformProperty + "-origin", xTransformOrigin + " " + yTransformOrigin); this.adapter.setStyleProperty('top', top + "px"); this.adapter.setStyleProperty('left', left + "px"); }; MDCTooltipFoundation.prototype.positionRichTooltip = function () { // TODO(b/177686782): Remove width setting when max-content is used to style // the rich tooltip. var _a, _b, _c, _d; // getComputedStyleProperty is used instead of getTooltipSize since // getTooltipSize returns the offSetWidth, which includes the border and // padding. What we need is the width of the tooltip without border and // padding. var width = this.adapter.getComputedStyleProperty('width'); // When rich tooltips are positioned within their parent containers, the // tooltip width might be shrunk if it collides with the edge of the parent // container. We set the width of the tooltip to prevent this. this.adapter.setStyleProperty('width', width); var _e = this.hasCaret ? this.calculateTooltipWithCaretStyles(this.anchorRect) : this.calculateTooltipStyles(this.anchorRect), top = _e.top, yTransformOrigin = _e.yTransformOrigin, left = _e.left, xTransformOrigin = _e.xTransformOrigin; var transformProperty = HAS_WINDOW ? getCorrectPropertyName(window, 'transform') : 'transform'; this.adapter.setSurfaceAnimationStyleProperty(transformProperty + "-origin", xTransformOrigin + " " + yTransformOrigin); // A rich tooltip has `absolute` positioning and is placed as a sibling to // the anchor element. Its positioning is calculated with respect to the // parent element, and so the values need to be adjusted against the parent // element. var leftAdjustment = left - ((_b = (_a = this.parentRect) === null || _a === void 0 ? void 0 : _a.left) !== null && _b !== void 0 ? _b : 0); var topAdjustment = top - ((_d = (_c = this.parentRect) === null || _c === void 0 ? void 0 : _c.top) !== null && _d !== void 0 ? _d : 0); this.adapter.setStyleProperty('top', topAdjustment + "px"); this.adapter.setStyleProperty('left', leftAdjustment + "px"); }; /** * Calculates the position of the tooltip. A tooltip will be placed beneath * the anchor element and aligned either with the 'start'/'end' edge of the * anchor element or the 'center'. * * Tooltip alignment is selected such that the tooltip maintains a threshold * distance away from the viewport (defaulting to 'center' alignment). If the * placement of the anchor prevents this threshold distance from being * maintained, the tooltip is positioned so that it does not collide with the * viewport. * * Users can specify an alignment, however, if this alignment results in the * tooltip colliding with the viewport, this specification is overwritten. */ MDCTooltipFoundation.prototype.calculateTooltipStyles = function (anchorRect) { if (!anchorRect) { return { top: 0, left: 0 }; } var tooltipSize = this.adapter.getTooltipSize(); var top = this.calculateYTooltipDistance(anchorRect, tooltipSize.height); var left = this.calculateXTooltipDistance(anchorRect, tooltipSize.width); return { top: top.distance, yTransformOrigin: top.yTransformOrigin, left: left.distance, xTransformOrigin: left.xTransformOrigin }; }; /** * Calculates the `left` distance for the tooltip. * Returns the distance value and a string indicating the x-axis transform- * origin that should be used when animating the tooltip. */ MDCTooltipFoundation.prototype.calculateXTooltipDistance = function (anchorRect, tooltipWidth) { var isLTR = !this.adapter.isRTL(); var startPos, endPos, centerPos; var startTransformOrigin, endTransformOrigin; if (this.richTooltip) { startPos = isLTR ? anchorRect.left - tooltipWidth : anchorRect.right; endPos = isLTR ? anchorRect.right : anchorRect.left - tooltipWidth; startTransformOrigin = isLTR ? strings.RIGHT : strings.LEFT; endTransformOrigin = isLTR ? strings.LEFT : strings.RIGHT; } else { startPos = isLTR ? anchorRect.left : anchorRect.right - tooltipWidth; endPos = isLTR ? anchorRect.right - tooltipWidth : anchorRect.left; centerPos = anchorRect.left + (anchorRect.width - tooltipWidth) / 2; startTransformOrigin = isLTR ? strings.LEFT : strings.RIGHT; endTransformOrigin = isLTR ? strings.RIGHT : strings.LEFT; } var positionOptions = this.richTooltip ? this.determineValidPositionOptions(startPos, endPos) : // For plain tooltips, centerPos is defined this.determineValidPositionOptions(centerPos, startPos, endPos); if (this.xTooltipPos === XPosition.START && positionOptions.has(startPos)) { return { distance: startPos, xTransformOrigin: startTransformOrigin }; } if (this.xTooltipPos === XPosition.END && positionOptions.has(endPos)) { return { distance: endPos, xTransformOrigin: endTransformOrigin }; } if (this.xTooltipPos === XPosition.CENTER && positionOptions.has(centerPos)) { // This code path is only executed if calculating the distance for plain // tooltips. In this instance, centerPos will always be defined, so we can // safely assert that the returned value is non-null/undefined. return { distance: centerPos, xTransformOrigin: strings.CENTER }; } // If no user position is supplied, rich tooltips default to end pos, then // start position. Plain tooltips default to center, start, then end. var possiblePositions = this.richTooltip ? [ { distance: endPos, xTransformOrigin: endTransformOrigin }, { distance: startPos, xTransformOrigin: startTransformOrigin } ] : [ { distance: centerPos, xTransformOrigin: strings.CENTER }, { distance: startPos, xTransformOrigin: startTransformOrigin }, { distance: endPos, xTransformOrigin: endTransformOrigin } ]; var validPosition = possiblePositions.find(function (_a) { var distance = _a.distance; return positionOptions.has(distance); }); if (validPosition) { return validPosition; } // Indicates that all potential positions would result in the tooltip // colliding with the viewport. This would only occur when the anchor // element itself collides with the viewport, or the viewport is very // narrow. In this case, we allow the tooltip to be mis-aligned from the // anchor element. if (anchorRect.left < 0) { return { distance: this.minViewportTooltipThreshold, xTransformOrigin: strings.LEFT }; } else { var viewportWidth = this.adapter.getViewportWidth(); var distance = viewportWidth - (tooltipWidth + this.minViewportTooltipThreshold); return { distance: distance, xTransformOrigin: strings.RIGHT }; } }; /** * Given the values for the horizontal alignments of the tooltip, calculates * which of these options would result in the tooltip maintaining the required * threshold distance vs which would result in the tooltip staying within the * viewport. * * A Set of values is returned holding the distances that would honor the * above requirements. Following the logic for determining the tooltip * position, if all alignments violate the threshold, then the returned Set * contains values that keep the tooltip within the viewport. */ MDCTooltipFoundation.prototype.determineValidPositionOptions = function () { var e_3, _a; var positions = []; for (var _i = 0; _i < arguments.length; _i++) { positions[_i] = arguments[_i]; } var posWithinThreshold = new Set(); var posWithinViewport = new Set(); try { for (var positions_1 = __values(positions), positions_1_1 = positions_1.next(); !positions_1_1.done; positions_1_1 = positions_1.next()) { var position = positions_1_1.value; if (this.positionHonorsViewportThreshold(position)) { posWithinThreshold.add(position); } else if (this.positionDoesntCollideWithViewport(position)) { posWithinViewport.add(position); } } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (positions_1_1 && !positions_1_1.done && (_a = positions_1.return)) _a.call(positions_1); } finally { if (e_3) throw e_3.error; } } return posWithinThreshold.size ? posWithinThreshold : posWithinViewport; }; MDCTooltipFoundation.prototype.positionHonorsViewportThreshold = function (leftPos) { var viewportWidth = this.adapter.getViewportWidth(); var tooltipWidth = this.adapter.getTooltipSize().width; return leftPos + tooltipWidth <= viewportWidth - this.minViewportTooltipThreshold && leftPos >= this.minViewportTooltipThreshold; }; MDCTooltipFoundation.prototype.positionDoesntCollideWithViewport = function (leftPos) { var viewportWidth = this.adapter.getViewportWidth(); var tooltipWidth = this.adapter.getTooltipSize().width; return leftPos + tooltipWidth <= viewportWidth && leftPos >= 0; }; /** * Calculates the `top` distance for the tooltip. * Returns the distance value and a string indicating the y-axis transform- * origin that should be used when animating the tooltip. */ MDCTooltipFoundation.prototype.calculateYTooltipDistance = function (anchorRect, tooltipHeight) { var belowYPos = anchorRect.bottom + this.anchorGap; var aboveYPos = anchorRect.top - (this.anchorGap + tooltipHeight); var yPositionOptions = this.determineValidYPositionOptions(aboveYPos, belowYPos); if (this.yTooltipPos === YPosition.ABOVE && yPositionOptions.has(aboveYPos)) { return { distance: aboveYPos, yTransformOrigin: strings.BOTTOM }; } else if (this.yTooltipPos === YPosition.BELOW && yPositionOptions.has(belowYPos)) { return { distance: belowYPos, yTransformOrigin: strings.TOP }; } if (yPositionOptions.has(belowYPos)) { return { distance: belowYPos, yTransformOrigin: strings.TOP }; } if (yPositionOptions.has(aboveYPos)) { return { distance: aboveYPos, yTransformOrigin: strings.BOTTOM }; } // Indicates that all potential positions would result in the tooltip // colliding with the viewport. This would only occur when the viewport is // very short. return { distance: belowYPos, yTransformOrigin: strings.TOP }; }; /** * Given the values for above/below alignment of the tooltip, calculates * which of these options would result in the tooltip maintaining the required * threshold distance vs which would result in the tooltip staying within the * viewport. * * A Set of values is returned holding the distances that would honor the * above requirements. Following the logic for determining the tooltip * position, if all possible alignments violate the threshold, then the * returned Set contains values that keep the tooltip within the viewport. */ MDCTooltipFoundation.prototype.determineValidYPositionOptions = function (aboveAnchorPos, belowAnchorPos) { var posWithinThreshold = new Set(); var posWithinViewport = new Set(); if (this.yPositionHonorsViewportThreshold(aboveAnchorPos)) { posWithinThreshold.add(aboveAnchorPos); } else if (this.yPositionDoesntCollideWithViewport(aboveAnchorPos)) { posWithinViewport.add(aboveAnchorPos); } if (this.yPositionHonorsViewportThreshold(belowAnchorPos)) { posWithinThreshold.add(belowAnchorPos); } else if (this.yPositionDoesntCollideWithViewport(belowAnchorPos)) { posWithinViewport.add(belowAnchorPos); } return posWithinThreshold.size ? posWithinThreshold : posWithinViewport; }; MDCTooltipFoundation.prototype.yPositionHonorsViewportThreshold = function (yPos) { var viewportHeight = this.adapter.getViewportHeight(); var tooltipHeight = this.adapter.getTooltipSize().height; return yPos + tooltipHeight + this.minViewportTooltipThreshold <= viewportHeight && yPos >= this.minViewportTooltipThreshold; }; MDCTooltipFoundation.prototype.yPositionDoesntCollideWithViewport = function (yPos) { var viewportHeight = this.adapter.getViewportHeight(); var tooltipHeight = this.adapter.getTooltipSize().height; return yPos + tooltipHeight <= viewportHeight && yPos >= 0; }; MDCTooltipFoundation.prototype.calculateTooltipWithCaretStyles = function (anchorRect) { // Prior to grabbing the caret bounding rect, we clear all styles set on the // caret. This will ensure the width/height is consistent (since we rotate // the caret 90deg in some positions which would result in the height and // width bounding rect measurements flipping). this.adapter.clearTooltipCaretStyles(); var caretSize = this.adapter.getTooltipCaretBoundingRect(); if (!anchorRect || !caretSize) { return { position: PositionWithCaret.DETECTED, top: 0, left: 0 }; } // The caret for the rich tooltip is created by rotating/skewing/scaling // square div into a diamond shape and then hiding half of it so it looks // like a triangle. We use the boundingClientRect to calculate the // width/height of the element after the transforms (to the caret) have been // applied. Since the full tooltip is scaled by 0.8 for the entrance // animation, we divide by this value to retrieve the actual caret // dimensions. var caretWidth = caretSize.width / numbers.ANIMATION_SCALE; // Since we hide half of caret, we divide the returned DOMRect height // by 2. var caretHeight = (caretSize.height / numbers.ANIMATION_SCALE) / 2; var tooltipSize = this.adapter.getTooltipSize(); var yOptions = this.calculateYWithCaretDistanceOptions(anchorRect, tooltipSize.height, { caretWidth: caretWidth, caretHeight: caretHeight }); var xOptions = this.calculateXWithCaretDistanceOptions(anchorRect, tooltipSize.width, { caretWidth: caretWidth, caretHeight: caretHeight }); var positionOptions = this.validateTooltipWithCaretDistances(yOptions, xOptions); if (positionOptions.size < 1) { positionOptions = this.generateBackupPositionOption(anchorRect, tooltipSize, { caretWidth: caretWidth, caretHeight: caretHeight }); } var _a = this.determineTooltipWithCaretDistance(positionOptions), position = _a.position, xDistance = _a.xDistance, yDistance = _a.yDistance; // After determining the position of the tooltip relative to the anchor, // place the caret in the corresponding position and retrieve the necessary // x/y transform origins needed to properly animate the tooltip entrance. var _b = this.setCaretPositionStyles(position, { caretWidth: caretWidth, caretHeight: caretHeight }), yTransformOrigin = _b.yTransformOrigin, xTransformOrigin = _b.xTransformOrigin; return { yTransformOrigin: yTransformOrigin, xTransformOrigin: xTransformOrigin, top: yDistance, left: xDistance }; }; MDCTooltipFoundation.prototype.calculateXWithCaretDistanceOptions = function (anchorRect, tooltipWidth, caretSize) { var caretWidth = caretSize.caretWidth, caretHeight = caretSize.caretHeight; var isLTR = !this.adapter.isRTL(); var anchorMidpoint = anchorRect.left + anchorRect.width / 2; var sideLeftAligned = anchorRect.left - (tooltipWidth + this.anchorGap + caretHeight); var sideRightAligned = anchorRect.right + this.anchorGap + caretHeight; var sideStartPos = isLTR ? sideLeftAligned : sideRightAligned; var sideEndPos = isLTR ? sideRightAligned : sideLeftAligned; var verticalLeftAligned = anchorMidpoint - (numbers.CARET_INDENTATION + caretWidth / 2); var verticalRightAligned = anchorMidpoint - (tooltipWidth - numbers.CARET_INDENTATION - caretWidth / 2); var verticalStartPos = isLTR ? verticalLeftAligned : verticalRightAligned; var verticalEndPos = isLTR ? verticalRightAligned : verticalLeftAligned; var verticalCenterPos = anchorMidpoint - tooltipWidth / 2; var possiblePositionsMap = new Map([ [XPositionWithCaret.START, verticalStartPos], [XPositionWithCaret.CENTER, verticalCenterPos], [XPositionWithCaret.END, verticalEndPos], [XPositionWithCaret.SIDE_END, sideEndPos], [XPositionWithCaret.SIDE_START, sideStartPos], ]); return possiblePositionsMap; }; MDCTooltipFoundation.prototype.calculateYWithCaretDistanceOptions = function (anchorRect, tooltipHeight, caretSize) { var caretWidth = caretSize.caretWidth, caretHeight = caretSize.caretHeight; var anchorMidpoint = anchorRect.top + anchorRect.height / 2; var belowYPos = anchorRect.bottom + this.anchorGap + caretHeight; var aboveYPos = anchorRect.top - (this.anchorGap + tooltipHeight + caretHeight); var sideTopYPos = anchorMidpoint - (numbers.CARET_INDENTATION + caretWidth / 2); var sideCenterYPos = anchorMidpoint - (tooltipHeight / 2); var sideBottomYPos = anchorMidpoint - (tooltipHeight - numbers.CARET_INDENTATION - caretWidth / 2); var possiblePositionsMap = new Map([ [YPositionWithCaret.ABOVE, aboveYPos], [YPositionWithCaret.BELOW, belowYPos], [YPositionWithCaret.SIDE_TOP, sideTopYPos], [YPositionWithCaret.SIDE_CENTER, sideCenterYPos], [YPositionWithCaret.SIDE_BOTTOM, sideBottomYPos], ]); return possiblePositionsMap; }; MDCTooltipFoundation.prototype.repositionTooltipOnAnchorMove = function () { var newAnchorRect = this.adapter.getAnchorBoundingRect(); if (!newAnchorRect || !this.anchorRect) return; if (newAnchorRect.top !== this.anchorRect.top || newAnchorRect.left !== this.anchorRect.left || newAnchorRect.height !== this.anchorRect.height || newAnchorRect.width !== this.anchorRect.width) { this.anchorRect = newAnchorRect; this.parentRect = this.adapter.getParentBoundingRect(); this.richTooltip ? this.positionRichTooltip() : this.positionPlainTooltip(); } }; /** * Given a list of x/y position options for a rich tooltip with caret, checks * if valid x/y combinations of these position options are either within the * viewport threshold, or simply within the viewport. Returns a map with the * valid x/y position combinations that all either honor the viewport * threshold or are simply inside within the viewport. */ MDCTooltipFoundation.prototype.validateTooltipWithCaretDistances = function (yOptions, xOptions) { var e_4, _a, e_5, _b, e_6, _c; var posWithinThreshold = new Map(); var posWithinViewport = new Map(); // If a tooltip has a caret, not all combinations of YPositionWithCarets and // XPositionWithCarets are possible. Because of this we only check the // validity of a given XPositionWithCaret if a potential corresponding // YPositionWithCaret is valid. var validMappings = new Map([ [ YPositionWithCaret.ABOVE, [ XPositionWithCaret.START, XPositionWithCaret.CENTER, XPositionWithCaret.END ] ], [ YPositionWithCaret.BELOW, [ XPositionWithCaret.START, XPositionWithCaret.CENTER, XPositionWithCaret.END ] ], [ YPositionWithCaret.SIDE_TOP, [XPositionWithCaret.SIDE_START, XPositionWithCaret.SIDE_END] ], [ YPositionWithCaret.SIDE_CENTER, [XPositionWithCaret.SIDE_START, XPositionWithCaret.SIDE_END] ], [ YPositionWithCaret.SIDE_BOTTOM, [XPositionWithCaret.SIDE_START, XPositionWithCaret.SIDE_END] ], ]); try { // TODO(b/227383292): Handle instances where one direction can fit in // in the viewport (whether honoring the threshold or not), and the // other direction does not. for (var _d = __values(validMappings.keys()), _e = _d.next(); !_e.done; _e = _d.next()) { var y = _e.value; var yDistance = yOptions.get(y); if (this.yPositionHonorsViewportThreshold(yDistance)) { try { for (var _f = (e_5 = void 0, __values(validMappings.get(y))), _g = _f.next(); !_g.done; _g = _f.next()) { var x = _g.value; var xDistance = xOptions.get(x); if (this.positionHonorsViewportThreshold(xDistance)) { var caretPositionName = this.caretPositionOptionsMapping(x, y); posWithinThreshold.set(caretPositionName, { xDistance: xDistance, yDistance: yDistance }); } } } catch (e_5_1) { e_5 = { error: e_5_1 }; } finally { try { if (_g && !_g.done && (_b = _f.return)) _b.call(_f); } finally { if (e_5) throw e_5.error; } } } if (this.yPositionDoesntCollideWithViewport(yDistance)) { try { for (var _h = (e_6 = void 0, __values(validMappings.get(y))), _j = _h.next(); !_j.done; _j = _h.next()) { var x = _j.value; var xDistance = xOptions.get(x); if (this.positionDoesntCollideWithViewport(xDistance)) { var caretPositionName = this.caretPositionOptionsMapping(x, y); posWithinViewport.set(caretPositionName, { xDistance: xDistance, yDistance: yDistance }); } } } catch (e_6_1) { e_6 = { error: e_6_1 }; } finally { try { if (_j && !_j.done && (_c = _h.return)) _c.call(_h); } finally { if (e_6) throw e_6.error; } } } } } catch (e_4_1) { e_4 = { error: e_4_1 }; } finally { try { if (_e && !_e.done && (_a = _d.return)) _a.call(_d); } finally { if (e_4) throw e_4.error; } } return posWithinThreshold.size ? posWithinThreshold : posWithinViewport; }; /** * Method for generating a horizontal and vertical position for the tooltip if * all other calculated values are considered invalid. This would only happen * in situations of very small viewports/large tooltips. */ MDCTooltipFoundation.prototype.generateBackupPositionOption = function (anchorRect, tooltipSize, caretSize) { var isLTR = !this.adapter.isRTL(); var xDistance; var xPos; if (anchorRect.left < 0) { xDistance = this.minViewportTooltipThreshold + caretSize.caretHeight; xPos = isLTR ? XPositionWithCaret.END : XPositionWithCaret.START; } else { var viewportWidth = this.adapter.getViewportWidth(); xDistance = viewportWidth - (tooltipSize.width + this.minViewportTooltipThreshold + caretSize.caretHeight); xPos = isLTR ? XPositionWithCaret.START : XPositionWithCaret.END; } var yDistance; var yPos; if (anchorRect.top < 0) { yDistance = this.minViewportTooltipThreshold + caretSize.caretHeight; yPos = YPositionWithCaret.BELOW; } else { var viewportHeight = this.adapter.getViewportHeight(); yDistance = viewportHeight - (tooltipSize.height + this.minViewportTooltipThreshold + caretSize.caretHeight); yPos = YPositionWithCaret.ABOVE; } var caretPositionName = this.caretPositionOptionsMapping(xPos, yPos); return new Map([[caretPositionName, { xDistance: xDistance, yDistance: yDistance }]]); }; /** * Given a list of valid position options for a rich tooltip with caret, * returns the option that should be used. */ MDCTooltipFoundation.prototype.determineTooltipWithCaretDistance = function (options) { if (options.has(this.tooltipPositionWithCaret)) { var tooltipPos = options.get(this