UNPKG

@atlassian/aui

Version:

Atlassian User Interface library

1,084 lines (926 loc) 35.5 kB
import { I18n } from './i18n'; import './spinner'; import $ from './jquery'; import template from 'skatejs-template-html'; import * as deprecate from './internal/deprecation'; import * as logger from './internal/log'; import { debounce } from 'underscore'; import { supportsVoiceOver } from './internal/browser'; import Alignment from './internal/alignment'; import CustomEvent from './polyfills/custom-event'; import keyCode from './key-code'; import layer from './layer'; import state from './internal/state'; import skate from './internal/skate'; import escapeHtml from './escape-html'; import {ifGone} from './internal/elements'; import {doIfTrigger, setTrigger} from './trigger'; import generateUniqueId from './unique-id'; function isHidden(el) { return el.hasAttribute('hidden') || el.classList.contains('hidden'); } function setDropdownTriggerActiveState(trigger, isActive) { trigger.setAttribute('aria-expanded', !!isActive); trigger.classList[isActive ? 'add' : 'remove']('active', 'aui-dropdown2-active'); } /** * Focus the appropriate thing when a dropdown is shown. * The element focussed will vary depending on the type of dropdown and * the input modality (e.g., whether it was an event 100% attributable to a hardware mouse or keyboard). * * @param {HTMLElement} dropdown - the dropdown being shown * @param {Event} [e] - the event that triggered the dropdown being shown */ function handleFocus(dropdown, e = {}) { const mouseEvent = e && e.type && (e.type.indexOf('mouse') > -1 || e.type.indexOf('hover') > -1); dropdown.isSubmenu && !mouseEvent ? dropdown.focusItem(0) : dropdown.focus(); } // LOADING STATES var UNLOADED = 'unloaded'; var LOADING = 'loading'; var ERROR = 'error'; var SUCCESS = 'success'; // ASYNC DROPDOWN FUNCTIONS function makeAsyncDropdownContents (json) { var dropdownContents = json.map(function makeSection (sectionData) { var sectionItemsHtml = sectionData.items.map(function makeSectionItem (itemData) { function makeBooleanAttribute (attr) { return itemData[attr] ? `${attr} ="true"` : ''; } function makeAttribute (attr) { return itemData[attr] ? `${attr}="${itemData[attr]}"` : ''; } var tagName = 'aui-item-' + itemData.type; var itemHtml = ` <${tagName} ${makeAttribute('for')} ${makeAttribute('href')} ${makeBooleanAttribute('interactive')} ${makeBooleanAttribute('checked')} ${makeBooleanAttribute('disabled')} ${makeBooleanAttribute('hidden')}> ${escapeHtml(itemData.content)} </${tagName}>`; return itemHtml; }).join(''); var sectionAttributes = sectionData.label ? `label="${sectionData.label}"` : ''; var sectionHtml = ` <aui-section ${sectionAttributes}> ${sectionItemsHtml} </aui-section>`; return sectionHtml; }).join('\n'); return dropdownContents; } function setDropdownContents (dropdown, json) { state(dropdown).set('loading-state', SUCCESS); dropdown.innerHTML = makeAsyncDropdownContents(json); skate.init(dropdown); /* TODO: where to check for focus * to make the loading state more accessible, we might need to move focus inside the dropdown container, * which would change the check from "focus is on trigger" to "focus is within dropdown". */ doIfTrigger(dropdown, function(trigger) { // Only manage focus if the focus was on the triggering element at the time it loaded. // Otherwise, the user likely moved on and doesn't want it any more. if (document.activeElement === trigger) { handleFocus(dropdown); } }); } function setDropdownErrorState (dropdown) { state(dropdown).set('loading-state', ERROR); state(dropdown).set('hasErrorBeenShown', dropdown.isVisible()); dropdown.innerHTML = ` <div class="aui-message aui-message-error aui-dropdown-error"> <p>${I18n.getText('aui.dropdown.async.error')}</p> </div> `; } function setDropdownLoadingState (dropdown) { state(dropdown).set('loading-state', LOADING); state(dropdown).set('hasErrorBeenShown', false); doIfTrigger(dropdown, function (trigger) { trigger.setAttribute('aria-busy', 'true'); }); dropdown.innerHTML = ` <div class="aui-dropdown-loading"> <aui-spinner size="small"></aui-spinner> ${I18n.getText('aui.dropdown.async.loading')} </div> `; } function setDropdownLoaded (dropdown) { doIfTrigger(dropdown, function (trigger) { trigger.setAttribute('aria-busy', 'false'); }); } function loadContentsIfAsync (dropdown) { if (!dropdown.src || state(dropdown).get('loading-state') === LOADING) { return; } setDropdownLoadingState(dropdown); $.ajax(dropdown.src) .done(function (json, status, xhr) { var isValidStatus = xhr.status === 200; if (isValidStatus) { setDropdownContents(dropdown, json); } else { setDropdownErrorState(dropdown); } }) .fail(function () { setDropdownErrorState(dropdown); }) .always(function () { setDropdownLoaded(dropdown); }); } function loadContentWhenMouseEnterTrigger(dropdown) { var isDropdownUnloaded = state(dropdown).get('loading-state') === UNLOADED; var hasCurrentErrorBeenShown = state(dropdown).get('hasErrorBeenShown'); if (isDropdownUnloaded || hasCurrentErrorBeenShown && !dropdown.isVisible()) { loadContentsIfAsync(dropdown); } } function loadContentWhenMenuShown(dropdown) { var isDropdownUnloaded = state(dropdown).get('loading-state') === UNLOADED; var hasCurrentErrorBeenShown = state(dropdown).get('hasErrorBeenShown'); if (isDropdownUnloaded || hasCurrentErrorBeenShown) { loadContentsIfAsync(dropdown); } if (state(dropdown).get('loading-state') === ERROR) { state(dropdown).set('hasErrorBeenShown', true); } } // The dropdown's trigger // ---------------------- function triggerCreated (trigger) { let dropdownID = trigger.getAttribute('aria-controls'); if (!dropdownID) { dropdownID = trigger.getAttribute('aria-owns'); if (!dropdownID) { logger.error('Dropdown triggers need either a "aria-owns" or "aria-controls" attribute'); } else { trigger.removeAttribute('aria-owns'); trigger.setAttribute('aria-controls', dropdownID); } } trigger.setAttribute('aria-haspopup', true); trigger.setAttribute('aria-expanded', false); const shouldSetHref = trigger.nodeName === 'A' && !trigger.href; if (shouldSetHref) { trigger.setAttribute('href', `#${dropdownID}`); } function handleIt(e, forceShow = false) { if (e.currentTarget !== trigger) { return; } e.preventDefault(); if (!trigger.isEnabled()) { return; } const dropdown = document.getElementById(dropdownID); if (!dropdown) { logger.error('Could not find a dropdown with id "' + dropdownID + '" in the DOM.'); return; } // AUI-4271 - Maintains legacy integration with parent elements. const $trigger = $(trigger); if ($trigger.parent().hasClass('aui-buttons')) { dropdown.classList.add('aui-dropdown2-in-buttons'); } if ($trigger.parents().hasClass('aui-header')) { dropdown.classList.add('aui-dropdown2-in-header'); } if (forceShow) { dropdown.show(e); } else { dropdown.toggle(e); } dropdown.isSubmenu = trigger.hasSubmenu(); return dropdown; } function handleMouseEnter(e) { if (e.currentTarget !== trigger) { return; } e.preventDefault(); if (!trigger.isEnabled()) { return; } const dropdown = document.getElementById(dropdownID); if (!dropdown) { logger.error('Could not find a dropdown with id "' + dropdownID + '" in the DOM.'); return; } loadContentWhenMouseEnterTrigger(dropdown); if (trigger.hasSubmenu()) { dropdown.show(e); dropdown.isSubmenu = trigger.hasSubmenu(); } return dropdown; } function handleKeydown(e) { if (e.currentTarget !== trigger) { return; } const normalInvoke = (e.keyCode === keyCode.ENTER || e.keyCode === keyCode.SPACE); const submenuInvoke = (e.keyCode === keyCode.RIGHT && trigger.hasSubmenu()); const rootMenuInvoke = ((e.keyCode === keyCode.UP || e.keyCode === keyCode.DOWN) && !trigger.hasSubmenu()); if (normalInvoke) { handleIt(e); } else if (rootMenuInvoke || submenuInvoke) { // this could be the first keyboard event after using an AT shortcut to open the dropdown. handleIt(e, document.activeElement === trigger); } } $(trigger) .on('aui-button-invoke', handleIt) .on('click', handleIt) .on('keydown', handleKeydown) .on('mouseenter', handleMouseEnter) ; } var triggerPrototype = { disable: function () { this.setAttribute('aria-disabled', 'true'); this.classList.add('disabled', 'aui-dropdown2-disabled'); }, enable: function () { this.setAttribute('aria-disabled', 'false'); this.classList.remove('disabled', 'aui-dropdown2-disabled'); }, isEnabled: function () { return this.getAttribute('aria-disabled') !== 'true' && this.classList.contains('aui-dropdown2-disabled') === false; }, hasSubmenu: function () { return this.classList.contains('aui-dropdown2-sub-trigger'); } }; //To remove at a later date. Some dropdown triggers initialise lazily, so we need to listen for mousedown //and synchronously init before the click event is fired. //TODO: delete in AUI 8.0.0, see AUI-2868 function bindLazyTriggerInitialisation() { $(document).on('mousedown', '.aui-dropdown2-trigger', function () { var isElementSkated = this.hasAttribute('resolved'); if (!isElementSkated) { skate.init(this); var lazyDeprecate = deprecate.getMessageLogger('Dropdown2 lazy initialisation', { removeInVersion: '10.0.0', alternativeName: 'initialisation on DOM insertion', sinceVersion: '5.8.0', extraInfo: 'Dropdown2 triggers should have all necessary attributes on DOM insertion', deprecationType: 'JS' }); lazyDeprecate(); } }); } bindLazyTriggerInitialisation(); // Dropdown trigger groups // ----------------------- $(document).on('mouseenter', '.aui-dropdown2-trigger-group a, .aui-dropdown2-trigger-group button', function (e) { const $item = $(e.currentTarget); if ($item.is('.aui-dropdown2-active')) { return; // No point doing anything if we're hovering over the already-active item trigger. } if ($item.closest('.aui-dropdown2').length) { return; // We don't want to deal with dropdown items, just the potential triggers in the group. } const $triggerGroup = $item.closest('.aui-dropdown2-trigger-group'); const $groupActiveTrigger = $triggerGroup.find('.aui-dropdown2-active'); if ($groupActiveTrigger.length && $item.is('.aui-dropdown2-trigger')) { $groupActiveTrigger.blur(); // Remove focus from the previously opened menu. $item.trigger('aui-button-invoke'); // Open this trigger's menu. e.preventDefault(); } const $groupFocusedTrigger = $triggerGroup.find(':focus'); if ($groupFocusedTrigger.length && $item.is('.aui-dropdown2-trigger')) { $groupFocusedTrigger.blur(); } }); // Dropdown items // -------------- function getDropdownItems (dropdown, filter) { return $(dropdown) .find([ // Legacy markup. '> ul > li', '> .aui-dropdown2-section > ul > li', '> .aui-dropdown2-section > div[role="group"] > ul > li', '> div > .aui-dropdown2-section > div[role="group"] > ul > li', // Web component. 'aui-item-link', 'aui-item-checkbox', 'aui-item-radio' ].join(', ')) .filter(filter) .children('a, button, [role="checkbox"], [role="menuitemcheckbox"], [role="radio"], [role="menuitemradio"]'); } function getAllDropdownItems (dropdown) { return getDropdownItems(dropdown, () => true); } function getVisibleDropdownItems (dropdown) { return getDropdownItems(dropdown, (i, el) => !isHidden(el)); } function amendDropdownItem (item) { item.setAttribute('tabindex', '-1'); /** * Honouring the documentation. * @link https://aui.atlassian.com/latest/docs/dropdown2.html */ if (item.classList.contains('aui-dropdown2-disabled') || isHidden(item.parentElement)) { item.setAttribute('aria-disabled', 'true'); } } function amendDropdownContent (dropdown) { // Add assistive semantics to each dropdown item getAllDropdownItems(dropdown).each((i, el) => amendDropdownItem(el)); } /** * Honours behaviour for code written using only the legacy class names. * To maintain old behaviour (i.e., remove the 'hidden' class and the item will become un-hidden) * whilst allowing our code to only depend on the new classes, we need to * keep the state of the DOM in sync with legacy classes. * * Calling this function will add the new namespaced classes to elements with legacy names. */ function migrateAndSyncLegacyClassNames (dropdown) { var $dropdown = $(dropdown); // Migrate away from legacy, unprefixed class names ['disabled', 'interactive', 'active', 'checked'].forEach(type => { $dropdown.find(`.${type}`).addClass(`aui-dropdown2-${type}`); }); } // The Dropdown itself // ------------------- function destroyAlignment(dropdown) { if (dropdown._auiAlignment) { dropdown._auiAlignment.destroy(); delete dropdown._auiAlignment; } } function setLayerAlignment(dropdown, trigger) { var hasSubmenu = trigger && trigger.hasSubmenu && trigger.hasSubmenu(); var hasSubmenuAlignment = dropdown.getAttribute('data-aui-alignment') === 'submenu auto'; if (!hasSubmenu && hasSubmenuAlignment) { restorePreviousAlignment(dropdown); } var hasAnyAlignment = dropdown.hasAttribute('data-aui-alignment'); if (hasSubmenu && !hasSubmenuAlignment) { saveCurrentAlignment(dropdown); dropdown.setAttribute('data-aui-alignment', 'submenu auto'); dropdown.setAttribute('data-aui-alignment-static', true); } else if (!hasAnyAlignment) { dropdown.setAttribute('data-aui-alignment', 'bottom auto'); dropdown.setAttribute('data-aui-alignment-static', true); } destroyAlignment(dropdown); dropdown._auiAlignment = new Alignment(dropdown, trigger, { flip: false, positionFixed: false, preventOverflow: false, // space between a dropdown trigger and the dropdown itself, based on @aui-dropdown-trigger-offset // only added for dropdowns which are not submenus offset: (trigger.hasSubmenu && trigger.hasSubmenu()) ? [-3, 0] : [0, 3] }); dropdown._auiAlignment.enable(); } function saveCurrentAlignment(dropdown) { var $dropdown = $(dropdown); if (dropdown.hasAttribute('data-aui-alignment')) { $dropdown.data('previous-data-aui-alignment', dropdown.getAttribute('data-aui-alignment')); } $dropdown.data('had-data-aui-alignment-static', dropdown.hasAttribute('data-aui-alignment-static')); } function restorePreviousAlignment(dropdown) { var $dropdown = $(dropdown); var previousAlignment = $dropdown.data('previous-data-aui-alignment'); if (previousAlignment) { dropdown.setAttribute('data-aui-alignment', previousAlignment); } else { dropdown.removeAttribute('data-aui-alignment'); } $dropdown.removeData('previous-data-aui-alignment'); if (!$dropdown.data('had-data-aui-alignment-static')) { dropdown.removeAttribute('data-aui-alignment-static'); } $dropdown.removeData('had-data-aui-alignment-static'); } function getDropdownHideLocation(dropdown, trigger) { var possibleHome = trigger.getAttribute('data-dropdown2-hide-location'); return document.getElementById(possibleHome) || dropdown.parentNode; } var keyboardClose = false; function keyboardCloseDetected () { keyboardClose = true; } function wasProbablyClosedViaKeyboard () { var result = (keyboardClose === true); keyboardClose = false; return result; } function bindDropdownBehaviourToLayer(dropdown) { layer(dropdown); dropdown.addEventListener('aui-layer-show', function (e) { if (dropdown !== e.target) { return; } // fix legacy markup patterns migrateAndSyncLegacyClassNames(dropdown); amendDropdownContent(dropdown); // indicate the dropdown is open $(dropdown).trigger('aui-dropdown2-show'); doIfTrigger(dropdown, function (trigger) { setDropdownTriggerActiveState(trigger, true); dropdown._returnTo = getDropdownHideLocation(dropdown, trigger); }); }); dropdown.addEventListener('aui-layer-hide', function (e) { if (dropdown !== e.target) { return; } $(dropdown).trigger('aui-dropdown2-hide'); if (dropdown._auiAlignment) { dropdown._auiAlignment.destroy(); } if (dropdown._returnTo) { if (dropdown.parentNode && dropdown.parentNode !== dropdown._returnTo) { dropdown._returnTo.appendChild(dropdown); } } dropdown.classList.remove('aui-dropdown2-in-header', 'aui-dropdown2-in-buttons'); getAllDropdownItems(dropdown).removeClass('active aui-dropdown2-active'); doIfTrigger(dropdown, function (trigger) { if (wasProbablyClosedViaKeyboard()) { trigger.focus(); setDropdownTriggerActiveState(trigger, trigger.hasSubmenu && trigger.hasSubmenu()); } else { setDropdownTriggerActiveState(trigger, false); } }); // Gets set by submenu trigger invocation. Bad coupling point? delete dropdown.isSubmenu; setTrigger(dropdown, null); }); } function bindItemInteractionBehaviourToDropdown (dropdown) { var $dropdown = $(dropdown); function belongsToDropdown(item) { const $dd = $(item).closest('.aui-dropdown2, aui-dropdown-menu'); return $dd.get(0) === dropdown; } $dropdown.on('keydown', function (e) { if (!belongsToDropdown(e.target)) { return; } if (e.keyCode === keyCode.DOWN) { dropdown.focusNext(); e.preventDefault(); } else if (e.keyCode === keyCode.UP) { dropdown.focusPrevious(); e.preventDefault(); } else if (e.keyCode === keyCode.LEFT) { if (dropdown.isSubmenu) { keyboardCloseDetected(); dropdown.hide(e); e.preventDefault(); } } else if (e.keyCode === keyCode.ESCAPE) { // The closing will be handled by the LayerManager! keyboardCloseDetected(); } else if (e.keyCode === keyCode.TAB) { keyboardCloseDetected(); dropdown.hide(e); } }); const hideIfNotSubmenuAndNotInteractive = function(e) { var item = e.currentTarget; if (item.getAttribute('aria-disabled') === 'true') { e.preventDefault(); return; } const isSubmenuTrigger = e.currentTarget.hasSubmenu && e.currentTarget.hasSubmenu(); if (!isSubmenuTrigger && !item.classList.contains('aui-dropdown2-interactive')) { var theMenu = dropdown; do { var dd = layer(theMenu); theMenu = layer(theMenu).below(); if (dd.$el.is('.aui-dropdown2')) { dd.hide(e); } } while (theMenu); } }; $dropdown.on('click keydown', 'a, button, [role="menuitem"], [role="menuitemcheckbox"], [role="checkbox"], [role="menuitemradio"], [role="radio"]', function (e) { if (!belongsToDropdown(e.target)) { return; } const item = e.currentTarget; const eventKeyCode = e.keyCode; const isEnter = eventKeyCode === keyCode.ENTER; const isSpace = eventKeyCode === keyCode.SPACE; // AUI-4283: Accessibility - need to ignore enter on links/buttons so // that the dropdown remains visible to allow the click event to eventually fire. const itemIgnoresEnter = isEnter && $(item).is('a[href], button'); if (!itemIgnoresEnter && (e.type === 'click' || isEnter || isSpace)) { hideIfNotSubmenuAndNotInteractive(e); } }); // close a submenus when the mouse moves over items other than its trigger $dropdown.on('mouseenter', 'a, button, [role="menuitem"], [role="menuitemcheckbox"], [role="checkbox"], [role="menuitemradio"], [role="radio"]', function (e) { if (!belongsToDropdown(e.target)) { return; } var item = e.currentTarget; var hasSubmenu = item.hasSubmenu && item.hasSubmenu(); if (!e.isDefaultPrevented() && !hasSubmenu) { var maybeALayer = layer(dropdown).above(); if (maybeALayer) { layer(maybeALayer).hide(); } } }); } $(window).on('resize', debounce(function () { $('.aui-dropdown2').each(function (index, dropdown) { skate.init(dropdown); if (dropdown.isVisible()){ dropdown.hide(); } }); }, 1000, true)); // Dropdowns // --------- function dropdownCreated (dropdown) { dropdown.classList.add('aui-dropdown2'); dropdown.setAttribute('tabindex','-1'); if (dropdown.hasAttribute('data-container')) { dropdown.setAttribute('data-aui-alignment-container', dropdown.getAttribute('data-container')); dropdown.removeAttribute('data-container'); } bindDropdownBehaviourToLayer(dropdown); bindItemInteractionBehaviourToDropdown(dropdown); $(dropdown).on('click keydown', '.aui-dropdown2-checkbox', function (e) { if (e.type === 'click' || e.keyCode === keyCode.ENTER || e.keyCode === keyCode.SPACE) { let checkbox = this; if (e.isDefaultPrevented()) { return; } if (checkbox.isInteractive()) { e.preventDefault(); } if (checkbox.isEnabled()) { // toggle the checked state if (checkbox.isChecked()) { checkbox.uncheck(); } else { checkbox.check(); } } } }); $(dropdown).on('click keydown', '.aui-dropdown2-radio', function (e) { if (e.type === 'click' || e.keyCode === keyCode.ENTER || e.keyCode === keyCode.SPACE) { let radio = this; if (e.isDefaultPrevented()) { return; } if (radio.isInteractive()) { e.preventDefault(); } if (this.isEnabled() && this.isChecked() === false) { // toggle the checked state $(radio).closest('ul,[role=group]').find('.aui-dropdown2-checked').not(this).each(function () { this.uncheck(); }); radio.check(); } } }); } var dropdownPrototype = { /** * Toggles the visibility of the dropdown menu */ toggle: function (e) { if (this.isVisible()) { this.hide(e); } else { this.show(e); } }, /** * Explicitly shows the menu * * @returns {HTMLElement} */ show: function (e) { var dropdown = this; if (e && e.currentTarget && e.currentTarget.classList.contains('aui-dropdown2-trigger')) { setTrigger(dropdown, e.currentTarget); } layer(dropdown).show(); doIfTrigger(dropdown, function (trigger) { setLayerAlignment(dropdown, trigger); }); // Manage focus on the next tick. // the setTimeout avoids a full page scroll caused by: // 1) the layer code moving the element to the bottom of the DOM, and then // 2) the alignment module asynchronously rendering the element further up the page // this code should ideally listen and wait for both those things to be done... but // this assumption is cheaper. setTimeout(() => handleFocus(dropdown, e), 0); return this; }, /** * Explicitly hides the menu * * @returns {HTMLElement} */ hide: function () { layer(this).hide(); return this; }, /** * Shifts explicit focus to the next available item in the menu * * @returns {undefined} */ focusNext: function () { var $items = getVisibleDropdownItems(this); var selected = document.activeElement; var idx; if ($items.last()[0] !== selected) { idx = $items.toArray().indexOf(selected); this.focusItem($items.get(idx + 1)); } }, /** * Shifts explicit focus to the previous available item in the menu * * @returns {undefined} */ focusPrevious: function () { var $items = getVisibleDropdownItems(this); var selected = document.activeElement; var idx; if ($items.first()[0] !== selected) { idx = $items.toArray().indexOf(selected); this.focusItem($items.get(idx - 1)); } }, /** * Shifts explicit focus to the menu item matching the index param */ focusItem: function (item) { const $items = getVisibleDropdownItems(this); if (typeof item === 'number') { item = $items.get(item); } const $item = $(item); $item.focus(); $items.removeClass('active aui-dropdown2-active'); $item.addClass('active aui-dropdown2-active'); }, /** * Checks whether or not the menu is currently displayed * * @returns {Boolean} */ isVisible: function () { return layer(this).isVisible(); } }; // Web component API for dropdowns // ------------------------------- var disabledAttributeHandler = { created: function (element) { var a = element.children[0]; a.setAttribute('aria-disabled', 'true'); a.classList.add('disabled', 'aui-dropdown2-disabled'); }, removed: function (element) { var a = element.children[0]; a.setAttribute('aria-disabled', 'false'); a.classList.remove('disabled', 'aui-dropdown2-disabled'); } }; var interactiveAttributeHandler = { created: function (element) { var a = element.children[0]; a.classList.add('interactive', 'aui-dropdown2-interactive'); }, removed: function (element) { var a = element.children[0]; a.classList.remove('interactive', 'aui-dropdown2-interactive'); } }; var checkedAttributeHandler = { created: function (element) { var a = element.children[0]; a.classList.add('checked', 'aui-dropdown2-checked'); a.setAttribute('aria-checked', true); element.dispatchEvent(new CustomEvent('change', {bubbles: true})); }, removed: function (element) { var a = element.children[0]; a.classList.remove('checked', 'aui-dropdown2-checked'); a.setAttribute('aria-checked', false); element.dispatchEvent(new CustomEvent('change', {bubbles: true})); } }; var hiddenAttributeHandler = { created: function (element) { disabledAttributeHandler.created(element); }, removed: function (element) { disabledAttributeHandler.removed(element); } }; var stringAttributeHandlerGenerator = function(attrName) { return { fallback: function (element, change) { var a = element.children[0]; a.setAttribute(attrName, change.newValue); }, removed: function (element) { var a = element.children[0]; a.removeAttribute(attrName); } }; }; const convertCssClassesToArray = function (spaceDelimitedClasses = '') { return spaceDelimitedClasses.split(' ').map(str => str.trim()).filter(x => x); } const ItemLinkEl = skate('aui-item-link', { template: template( '<a role="menuitem" tabindex="-1"><content></content></a>' ), attributes: { disabled: disabledAttributeHandler, interactive: interactiveAttributeHandler, hidden: hiddenAttributeHandler, href: stringAttributeHandlerGenerator('href'), 'item-id': stringAttributeHandlerGenerator('id'), for: { created: function (element) { var anchor = element.children[0]; anchor.setAttribute('aria-controls', element.getAttribute('for')); $(anchor).addClass('aui-dropdown2-sub-trigger'); }, updated: function (element) { var anchor = element.children[0]; anchor.setAttribute('aria-controls', element.getAttribute('for')); }, removed: function (element) { var anchor = element.children[0]; anchor.removeAttribute('aria-controls'); $(anchor).removeClass('aui-dropdown2-sub-trigger'); } }, 'extra-classes': function (element, change) { const anchor = element.children[0]; if (change.oldValue) { anchor.classList.remove(...convertCssClassesToArray(change.oldValue)); } if (change.newValue) { anchor.classList.add(...convertCssClassesToArray(change.newValue)); } }, }, }); const [ItemCheckboxEl, ItemRadioEl] = ['checkbox', 'radio'].map(type => { return skate(`aui-item-${type}`, { template: template( `<span role="${type}" class="aui-dropdown2-${type}" tabindex="-1"><content></content></span>` ), attributes: { 'item-id': stringAttributeHandlerGenerator('id'), disabled: disabledAttributeHandler, interactive: interactiveAttributeHandler, checked: checkedAttributeHandler, hidden: hiddenAttributeHandler }, }); }); const SectionEl = skate('aui-section', { template: template(` <span aria-hidden="true" class="aui-dropdown2-heading"></span> <div class="aui-dropdown2-item-group" role="group"> <content></content> </div> `), attributes: { label: function (element, data) { const id = generateUniqueId(); var headingElement = element.children[0]; var groupElement = element.children[1]; headingElement.textContent = data.newValue; headingElement.id = id; groupElement.setAttribute('aria-labelledby', id); } }, created: function (element) { element.classList.add('aui-dropdown2-section'); } }); const DropdownEl = skate('aui-dropdown-menu', { created: function (dropdown) { dropdown.setAttribute('role', 'menu'); dropdown.className = 'aui-dropdown2'; layer(dropdown); state(dropdown).set('loading-state', UNLOADED); // Now skate the .aui-dropdown2 behaviour. skate.init(dropdown); }, detached: function (dropdown) { ifGone(dropdown).then(() => destroyAlignment(dropdown)); }, attributes: { src: {} }, prototype: dropdownPrototype, events: { 'aui-layer-show': loadContentWhenMenuShown } }); // Legacy dropdown inits // --------------------- skate('aui-dropdown2', { type: skate.type.CLASSNAME, created: dropdownCreated, prototype: dropdownPrototype }); skate('data-aui-dropdown2', { type: skate.type.ATTRIBUTE, created: dropdownCreated, prototype: dropdownPrototype }); skate('aui-dropdown2-trigger', { type: skate.type.CLASSNAME, created: triggerCreated, prototype: triggerPrototype }); skate('aui-dropdown2-sub-trigger', { type: skate.type.CLASSNAME, created: function (trigger) { trigger.classList.add('aui-dropdown2-trigger'); skate.init(trigger); } }); // Checkboxes and radios // --------------------- ['checkbox', 'radio'].map(type => { return skate(`aui-dropdown2-${type}`, { type: skate.type.CLASSNAME, created: function (checkbox) { // determine checked state based on any one of three attributes, then sync all of them. const checked = ( checkbox.getAttribute('aria-checked') === 'true' || checkbox.classList.contains('checked') || checkbox.classList.contains('aui-dropdown2-checked') ); checkbox.classList[checked ? 'add' : 'remove']('checked', 'aui-dropdown2-checked'); checkbox.setAttribute('aria-checked', checked); // make the element part of the natural tab order. checkbox.setAttribute('tabindex', '0'); // swap from the "menuitemX" role to plain role for VoiceOver if (supportsVoiceOver()) { checkbox.setAttribute('role',type); } }, prototype: { isEnabled: function () { return this.getAttribute('aria-disabled') !== 'true'; }, isChecked: function () { return this.getAttribute('aria-checked') === 'true'; }, isInteractive: function () { return this.classList.contains('aui-dropdown2-interactive'); }, uncheck: function () { if (this.parentNode.tagName.toLowerCase() === `aui-item-${type}`) { this.parentNode.removeAttribute('checked'); } $(this) .attr('aria-checked', 'false') .removeClass('checked aui-dropdown2-checked') .trigger('aui-dropdown2-item-uncheck'); }, check: function () { if (this.parentNode.tagName.toLowerCase() === `aui-item-${type}`) { this.parentNode.setAttribute('checked', ''); } $(this) .attr('aria-checked', 'true') .addClass('checked aui-dropdown2-checked') .trigger('aui-dropdown2-item-check'); } } }); }) export { DropdownEl, ItemLinkEl, ItemRadioEl, ItemCheckboxEl, SectionEl, };