UNPKG

@atlassian/aui

Version:

Atlassian User Interface library

385 lines (341 loc) 12.1 kB
import $ from './jquery'; import Alignment from './internal/alignment'; import amdify from './internal/amdify'; import attributes from './internal/attributes'; import enforce from './internal/enforcer'; import globalize from './internal/globalize'; import layer, {EVENT_PREFIX} from './layer'; import skate from './internal/skate'; import state from './internal/state'; import {doIfTrigger, forEachTrigger, getTrigger, setTrigger} from './trigger'; import {ifGone} from './internal/elements'; const DEFAULT_HOVEROUT_DELAY = 1000; function changeTrigger(element, newTrigger) { doIfTrigger(element, function(oldTrigger) { oldTrigger.setAttribute('aria-expanded', 'false'); newTrigger.setAttribute('aria-expanded', element.open); }); setTrigger(element, newTrigger); } function enableAlignment (element, trigger) { if (element._auiAlignment){ element._auiAlignment.changeTarget(trigger); element._auiAlignment.enable(); element._auiAlignment.scheduleUpdate(); } else { element._auiAlignment = new Alignment(element, trigger, { overflowContainer: element.getAttribute('contained-by') === 'viewport' ? 'viewport' : 'window' }); element._auiAlignment.enable(); } } function disableAlignment (element) { if (element._auiAlignment) { element._auiAlignment.disable(); } } function destroyAlignment (element) { if (element._auiAlignment) { element._auiAlignment.destroy(); delete element._auiAlignment; } } function handleMessage (element, message) { var messageTypeMap = { toggle: ['click'], hover: ['mouseenter', 'mouseleave', 'focus', 'blur'] }; var messageList = messageTypeMap[element.respondsTo]; if (messageList && messageList.indexOf(message.type) > -1) { messageHandler[message.type](element, message); } } var messageHandler = { click: function (element, e) { if (element.open && !layer(element).isPersistent()) { element.open = false; } else { changeTrigger(element, e.currentTarget); element.open = true; } }, mouseenter: function (element, e) { var newTrigger = e.currentTarget; if (newTrigger) { changeTrigger(element, newTrigger); enableAlignment(element, newTrigger); } if (!element.open) { element.open = true; } if (element._clearMouseleaveTimeout) { element._clearMouseleaveTimeout(); } }, mouseleave: function (element) { if (!element.open || layer(element).isPersistent()) { return; } if (element._clearMouseleaveTimeout) { element._clearMouseleaveTimeout(); } var timeout = setTimeout(function () { if (!state(element).get('mouse-inside')) { element.open = false; } }, DEFAULT_HOVEROUT_DELAY); element._clearMouseleaveTimeout = function () { clearTimeout(timeout); element._clearMouseleaveTimeout = null; }; }, focus: function (element, e) { if (!element.open) { changeTrigger(element, e.currentTarget); element.open = true; } }, blur: function (element) { if (!layer(element).isPersistent() && element.open) { element.open = false; } } }; function onMouseEnter(e) { var element = e.target; state(element).set('mouse-inside', true); element.message({ type: 'mouseenter' }); } function onMouseLeave(e) { var element = e.target; state(element).set('mouse-inside', false); element.message({ type: 'mouseleave' }); } function rebindMouseEvents(el) { state(el).set('mouse-inside', undefined); el.removeEventListener('mouseenter', onMouseEnter); el.removeEventListener('mouseleave', onMouseLeave); if (el.respondsTo === 'hover') { state(el).set('mouse-inside', false); el.addEventListener('mouseenter', onMouseEnter); el.addEventListener('mouseleave', onMouseLeave); } } function namespaceEvent(eventName, elId) { return `${eventName}.nested-layer-${elId}` } function handleNestedLayers(el) { let $el = $(el); const elId = el.id; const noNestedTriggers = e => { return $el.find(getTrigger(e.target)).length < 1; }; // Temporary timeout variable to resolve AUI-5025 issue // as described in further detail below. const selectCloseTimeout = 150; $(document) .on(namespaceEvent('aui-layer-show', elId), e => { if (noNestedTriggers(e)) { return; } $el.attr('persistent', ''); }) .on(namespaceEvent('aui-layer-hide', elId), e => { if (noNestedTriggers(e)) { return; } $el.removeAttr('persistent'); }) .on(namespaceEvent('select2-opening', elId), () => { $el.attr('persistent', ''); }) // Relates to solving AUI-5025 // Temporary solution to race condition with select2, // where this runs before select2's dropdown is closed. .on(namespaceEvent('select2-close', elId), () => { setTimeout(() => { $el.removeAttr('persistent'); }, selectCloseTimeout); }); } function cleanupNestedLayerHandler(elId) { $(document) .off(namespaceEvent('aui-layer-hide', elId)) .off(namespaceEvent('aui-layer-show', elId)) .off(namespaceEvent('select2-opening', elId)) .off(namespaceEvent('select2-close', elId)); } function cleanupOnHide(el) { cleanupNestedLayerHandler(el.id); disableAlignment(el); } function showInlineDialog(el) { layer(el).show(); if (layer(el).isVisible()) { $(el).on(`${EVENT_PREFIX}-hide`, () => cleanupOnHide(el)); handleNestedLayers(el); doIfTrigger(el, function (trigger) { enableAlignment(el, trigger); trigger.setAttribute('aria-expanded', 'true'); }); } else { el.open = false; } } function hideInlineDialog(el) { layer(el).hide(); if (!layer(el).isVisible()) { cleanupOnHide(el); doIfTrigger(el, function (trigger) { trigger.setAttribute('aria-expanded', 'false'); }); } else { el.open = true; } setTrigger(el, null); } function reflectOpenness(el) { const isInitalizing = !el.hasAttribute('aria-hidden'); const shouldBeOpen = el.hasAttribute('open'); if (isInitalizing || el.open !== shouldBeOpen) { if (shouldBeOpen) { state(el).set('is-processing-show', true); showInlineDialog(el); state(el).set('is-processing-show', false); } else { hideInlineDialog(el); } } } const RESPONDS_TO_ATTRIBUTE_ENUM = { attribute: 'responds-to', values: ['toggle', 'hover'], missingDefault: 'toggle', invalidDefault: 'toggle' }; const InlineDialogEl = skate('aui-inline-dialog', { prototype: { /** * Returns whether the inline dialog is open. */ get open() { return layer(this).isVisible(); }, /** * Opens or closes the inline dialog, returning whether the dialog is * open or closed as a result (since event handlers can prevent either * action). * * You should check the value of open after setting this * value since the before show/hide events may have prevented it. */ set open(value) { // TODO AUI-3726 Revisit double calls to canceled event handlers. // Explicitly calling reflectOpenness(…) in this setter means // that in native we'll get two sync calls to reflectOpenness(…) // and in polyfill one sync (here) and one async (attr change // handler). The latter of the two calls, for both cases, will // usually be a noop (except when show/hide events are cancelled). attributes.setBooleanAttribute(this, 'open', value); reflectOpenness(this); }, get persistent() { return this.hasAttribute('persistent'); }, set persistent(value) { attributes.setBooleanAttribute(this, 'persistent', value); }, get respondsTo() { var attr = RESPONDS_TO_ATTRIBUTE_ENUM.attribute; return attributes.computeEnumValue(RESPONDS_TO_ATTRIBUTE_ENUM, this.getAttribute(attr)); }, set respondsTo(value) { const oldComputedValue = this.respondsTo; attributes.setEnumAttribute(this, RESPONDS_TO_ATTRIBUTE_ENUM, value); if (oldComputedValue !== this.respondsTo) { rebindMouseEvents(this); } }, /** * Handles the receiving of a message from another component. * * @param {Object} msg The message to act on. * * @returns {HTMLElement} */ message: function (msg) { handleMessage(this, msg); return this; } }, created: function (element) { state(element).set('is-processing-show', false); }, attributes: { 'aria-hidden': function (element, change) { if (change.newValue === 'true') { doIfTrigger(element, function(trigger) { trigger.setAttribute('aria-expanded', 'false'); }); } // Whenever layer manager hides us, we need to sync the open attribute. attributes.setBooleanAttribute(element, 'open', change.newValue === 'false'); }, open: function (element) { // skate runs the created callback for attributes before the // element is attached to the DOM, so guard against that. if (document.body.contains(element)) { reflectOpenness(element); } }, 'responds-to': function (element, change) { const oldComputedValue = attributes.computeEnumValue(RESPONDS_TO_ATTRIBUTE_ENUM, change.oldValue); const newComputedValue = attributes.computeEnumValue(RESPONDS_TO_ATTRIBUTE_ENUM, change.newValue); if (oldComputedValue !== newComputedValue) { rebindMouseEvents(element); } } }, attached: function (element) { enforce(element).attributeExists('id'); if (element.hasAttribute('open')) { // show() can cause the element to be reattached (to the <body>), // so guard against a nested show() call that blows up the layer // manager (since it sees us pushing the same element twice). if (!state(element).get('is-processing-show')) { reflectOpenness(element); } } else { reflectOpenness(element); } rebindMouseEvents(element); doIfTrigger(element, function (trigger) { trigger.setAttribute('aria-expanded', element.open); }); forEachTrigger(element, function (trigger) { trigger.setAttribute('aria-haspopup', 'true'); }); }, detached: function (element) { ifGone(element).then(() => { destroyAlignment(element); forEachTrigger(element, function (trigger) { trigger.removeAttribute('aria-haspopup'); trigger.removeAttribute('aria-expanded'); }) }); }, template: function (element) { var elem = $('<div class="aui-inline-dialog-contents"></div>').append(element.childNodes); $(element) .addClass('aui-layer') .html(elem); } }); amdify('aui/inline-dialog2', InlineDialogEl); globalize('InlineDialog2', InlineDialogEl); export default InlineDialogEl;