var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to set private field on non-instance"); } privateMap.set(receiver, value); return value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to get private field on non-instance"); } return privateMap.get(receiver); }; var _abortController, _align, _side, _allowUpdatePosition; import { getAnchoredPosition } from '@primer/behaviors'; const TOOLTIP_OPEN_CLASS = 'tooltip-open'; const DIRECTION_CLASSES = [ 'tooltip-n', 'tooltip-s', 'tooltip-e', 'tooltip-w', 'tooltip-ne', 'tooltip-se', 'tooltip-nw', 'tooltip-sw' ]; class TooltipElement extends HTMLElement { constructor() { super(); _abortController.set(this, void 0); _align.set(this, 'center'); _side.set(this, 'outside-bottom'); _allowUpdatePosition.set(this, false); const shadow = this.attachShadow({ mode: 'open' }); shadow.innerHTML = ` `; } styles() { return ` :host { position: absolute; z-index: 1000000; padding: .5em .75em; font: normal normal 11px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; -webkit-font-smoothing: subpixel-antialiased; color: var(--color-fg-on-emphasis); text-align: center; text-decoration: none; text-shadow: none; text-transform: none; letter-spacing: normal; word-wrap: break-word; white-space: pre; background: var(--color-neutral-emphasis-plus); border-radius: 6px; opacity: 0; max-width: 250px; word-wrap: break-word; white-space: normal } :host:before{ position: absolute; z-index: 1000001; color: var(--color-neutral-emphasis-plus); content: ""; border: 6px solid transparent; opacity: 0 } @keyframes tooltip-appear { from { opacity: 0 } to { opacity: 1 } } :host:after{ position: absolute; display: block; right: 0; left: 0; height: 12px; content: "" } :host(.${TOOLTIP_OPEN_CLASS}), :host(.${TOOLTIP_OPEN_CLASS}):before { animation-name: tooltip-appear; animation-duration: .1s; animation-fill-mode: forwards; animation-timing-function: ease-in; animation-delay: .4s } :host(.tooltip-s):before, :host(.tooltip-se):before, :host(.tooltip-sw):before { right: 50%; bottom: 100%; margin-right: -6px; border-bottom-color: var(--color-neutral-emphasis-plus) } :host(.tooltip-s):after, :host(.tooltip-se):after, :host(.tooltip-sw):after { bottom: 100% } :host(.tooltip-n):before, :host(.tooltip-ne):before, :host(.tooltip-nw):before { top: 100%; right: 50%; margin-right: -6px; border-top-color: var(--color-neutral-emphasis-plus) } :host(.tooltip-n):after, :host(.tooltip-ne):after, :host(.tooltip-nw):after { top: 100% } :host(.tooltip-se):before, :host(.tooltip-ne):before { right: auto } :host(.tooltip-sw):before, :host(.tooltip-nw):before { right: 0; margin-right: 6px } :host(.tooltip-w):before { top: 50%; bottom: 50%; left: 100%; margin-top: -6px; border-left-color: var(--color-neutral-emphasis-plus) } :host(.tooltip-e):before { top: 50%; right: 100%; bottom: 50%; margin-top: -6px; border-right-color: var(--color-neutral-emphasis-plus) } `; } get htmlFor() { return this.getAttribute('for') || ''; } set htmlFor(value) { this.setAttribute('for', value); } get type() { const type = this.getAttribute('data-type'); return type === 'label' ? 'label' : 'description'; } set type(value) { this.setAttribute('data-type', value); } get direction() { return (this.getAttribute('data-direction') || 's'); } set direction(value) { this.setAttribute('data-direction', value); } get control() { return this.ownerDocument.getElementById(this.htmlFor); } connectedCallback() { var _a; this.hidden = true; __classPrivateFieldSet(this, _allowUpdatePosition, true); if (!this.id) { this.id = `tooltip-${Date.now()}-${(Math.random() * 10000).toFixed(0)}`; } if (!this.control) return; this.setAttribute('role', 'tooltip'); (_a = __classPrivateFieldGet(this, _abortController)) === null || _a === void 0 ? void 0 : _a.abort(); __classPrivateFieldSet(this, _abortController, new AbortController()); const { signal } = __classPrivateFieldGet(this, _abortController); this.addEventListener('mouseleave', this, { signal }); this.control.addEventListener('mouseenter', this, { signal }); this.control.addEventListener('mouseleave', this, { signal }); this.control.addEventListener('focus', this, { signal }); this.control.addEventListener('blur', this, { signal }); this.ownerDocument.addEventListener('keydown', this, { signal }); } disconnectedCallback() { var _a; (_a = __classPrivateFieldGet(this, _abortController)) === null || _a === void 0 ? void 0 : _a.abort(); } handleEvent(event) { if (!this.control) return; // Ensures that tooltip stays open when hovering between tooltip and element // WCAG Success Criterion 1.4.13 Hoverable if ((event.type === 'mouseenter' || event.type === 'focus') && this.hidden) { this.hidden = false; } else if (event.type === 'blur') { this.hidden = true; } else if (event.type === 'mouseleave' && event.relatedTarget !== this.control && event.relatedTarget !== this) { this.hidden = true; } else if (event.type === 'keydown' && event.key === 'Escape' && !this.hidden) { this.hidden = true; } } attributeChangedCallback(name) { if (name === 'id' || name === 'data-type') { if (!this.id || !this.control) return; if (this.type === 'label') { this.control.setAttribute('aria-labelledby', this.id); } else { let describedBy = this.control.getAttribute('aria-describedby'); describedBy ? (describedBy = `${describedBy} ${this.id}`) : (describedBy = this.id); this.control.setAttribute('aria-describedby', describedBy); } } else if (name === 'hidden') { if (this.hidden) { this.classList.remove(TOOLTIP_OPEN_CLASS, ...DIRECTION_CLASSES); } else { this.classList.add(TOOLTIP_OPEN_CLASS); for (const tooltip of this.ownerDocument.querySelectorAll(this.tagName)) { if (tooltip !== this) tooltip.hidden = true; } this..call(this); } } else if (name === 'data-direction') { this.classList.remove(...DIRECTION_CLASSES); const direction = this.direction; if (direction === 'n') { __classPrivateFieldSet(this, _align, 'center'); __classPrivateFieldSet(this, _side, 'outside-top'); } else if (direction === 'ne') { __classPrivateFieldSet(this, _align, 'start'); __classPrivateFieldSet(this, _side, 'outside-top'); } else if (direction === 'e') { __classPrivateFieldSet(this, _align, 'center'); __classPrivateFieldSet(this, _side, 'outside-right'); } else if (direction === 'se') { __classPrivateFieldSet(this, _align, 'start'); __classPrivateFieldSet(this, _side, 'outside-bottom'); } else if (direction === 's') { __classPrivateFieldSet(this, _align, 'center'); __classPrivateFieldSet(this, _side, 'outside-bottom'); } else if (direction === 'sw') { __classPrivateFieldSet(this, _align, 'end'); __classPrivateFieldSet(this, _side, 'outside-bottom'); } else if (direction === 'w') { __classPrivateFieldSet(this, _align, 'center'); __classPrivateFieldSet(this, _side, 'outside-left'); } else if (direction === 'nw') { __classPrivateFieldSet(this, _align, 'end'); __classPrivateFieldSet(this, _side, 'outside-top'); } } } // `getAnchoredPosition` may calibrate `anchoredSide` but does not recalibrate `align`. // Therefore, we need to determine which `align` is best based on the initial `getAnchoredPosition` calcluation. // Related: https://github.com/primer/behaviors/issues/63 (anchorSide) { if (!this.control) return; const tooltipPosition = this.getBoundingClientRect(); const targetPosition = this.control.getBoundingClientRect(); const tooltipWidth = tooltipPosition.width; const tooltipCenter = tooltipPosition.left + tooltipWidth / 2; const targetCenter = targetPosition.x + targetPosition.width / 2; if (Math.abs(tooltipCenter - targetCenter) < 2 || anchorSide === 'outside-left' || anchorSide === 'outside-right') { return 'center'; } else if (tooltipPosition.left === targetPosition.left) { return 'start'; } else if (tooltipPosition.right === targetPosition.right) { return 'end'; } else if (tooltipCenter < targetCenter) { if (tooltipPosition.left === 0) return 'start'; return 'end'; } else { if (tooltipPosition.right === 0) return 'end'; return 'start'; } } () { if (!this.control) return; if (!__classPrivateFieldGet(this, _allowUpdatePosition) || this.hidden) return; const TOOLTIP_OFFSET = 10; this.style.left = `0px`; // Ensures we have reliable tooltip width in `getAnchoredPosition` let position = getAnchoredPosition(this, this.control, { side: __classPrivateFieldGet(this, _side), align: __classPrivateFieldGet(this, _align), anchorOffset: TOOLTIP_OFFSET }); let anchorSide = position.anchorSide; // We need to set tooltip position in order to determine ideal align. this.style.top = `${position.top}px`; this.style.left = `${position.left}px`; let direction = 's'; const align = this..call(this, anchorSide); if (!align) return; this.style.left = `0px`; // Reset tooltip position again to ensure accurate width in `getAnchoredPosition` position = getAnchoredPosition(this, this.control, { side: anchorSide, align, anchorOffset: TOOLTIP_OFFSET }); anchorSide = position.anchorSide; this.style.top = `${position.top}px`; this.style.left = `${position.left}px`; if (anchorSide === 'outside-left') { direction = 'w'; } else if (anchorSide === 'outside-right') { direction = 'e'; } else if (anchorSide === 'outside-top') { if (align === 'center') { direction = 'n'; } else if (align === 'start') { direction = 'ne'; } else { direction = 'nw'; } } else { if (align === 'center') { direction = 's'; } else if (align === 'start') { direction = 'se'; } else { direction = 'sw'; } } this.classList.add(`tooltip-${direction}`); } } _abortController = new WeakMap(), _align = new WeakMap(), _side = new WeakMap(), _allowUpdatePosition = new WeakMap(); TooltipElement.observedAttributes = ['data-type', 'data-direction', 'id', 'hidden']; if (!window.customElements.get('tool-tip')) { window.TooltipElement = TooltipElement; window.customElements.define('tool-tip', TooltipElement); }