@atlassian/aui
Version:
Atlassian User Interface library
1,084 lines (926 loc) • 35.5 kB
JavaScript
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,
};