@atlassian/aui
Version:
Atlassian User Interface library
238 lines (195 loc) • 7.25 kB
JavaScript
import $ from './jquery';
import amdify from './internal/amdify';
import skate from './internal/skate';
import './spinner';
import { getMessageLogger } from './internal/deprecation';
const CLASS_NOTIFICATION_INITIALISED = '_aui-form-notification-initialised';
const ARIA_INFO_ATTRIBUTE = 'aria-describedby';
const ARIA_DESCRIPTION_POSTFIX = 'description';
const ARIA_ERROR_POSTFIX = 'errors';
const ATTRIBUTE_NOTIFICATION_PREFIX = 'data-aui-notification-';
const ATTRIBUTE_NOTIFICATION_WAIT = ATTRIBUTE_NOTIFICATION_PREFIX + 'wait';
const ATTRIBUTE_NOTIFICATION_INFO = ATTRIBUTE_NOTIFICATION_PREFIX + 'info';
const ATTRIBUTE_NOTIFICATION_ERROR = ATTRIBUTE_NOTIFICATION_PREFIX + 'error';
const ATTRIBUTE_NOTIFICATION_SUCCESS = ATTRIBUTE_NOTIFICATION_PREFIX + 'success';
const NOTIFICATION_PRIORITY = [
ATTRIBUTE_NOTIFICATION_ERROR,
ATTRIBUTE_NOTIFICATION_SUCCESS,
ATTRIBUTE_NOTIFICATION_WAIT,
ATTRIBUTE_NOTIFICATION_INFO,
];
function initialiseNotification($field) {
if (!isFieldInitialised($field)) {
prepareFieldMarkup($field);
synchroniseNotificationDisplay($field);
}
}
function isFieldInitialised($field) {
return $field.hasClass(CLASS_NOTIFICATION_INITIALISED);
}
function prepareFieldMarkup($field) {
$field.addClass(CLASS_NOTIFICATION_INITIALISED);
appendDescription($field);
}
function appendDescription($field, message) {
message = message ? message : getNotificationMessage($field);
if (getFieldNotificationType($field) === ATTRIBUTE_NOTIFICATION_INFO) {
const existingDescription = $field.parent().find('.description');
if (!existingDescription.length) {
$field.after(descriptionTemplate($field, message));
}
}
updateAriaInfo($field);
}
function getNotificationMessage($field) {
var notificationType = getFieldNotificationType($field);
var message = notificationType ? $field.attr(notificationType) : '';
return message === '' ? message : jsonToArray(message);
}
function jsonToArray(jsonOrString) {
var jsonArray;
try {
jsonArray = JSON.parse(jsonOrString);
// eslint-disable-next-line no-unused-vars
} catch (exception) {
jsonArray = [jsonOrString];
}
return jsonArray;
}
function getFieldNotificationType($field) {
let fieldNotificationType;
NOTIFICATION_PRIORITY.some(function (prioritisedNotification) {
if ($field.is('[' + prioritisedNotification + ']')) {
fieldNotificationType = prioritisedNotification;
return true;
}
});
return fieldNotificationType;
}
function synchroniseNotificationDisplay(field) {
const $field = $(field);
if (!isFieldInitialised($field)) {
return;
}
const type = getFieldNotificationType($field);
const showSpinner = type === ATTRIBUTE_NOTIFICATION_WAIT;
setFieldSpinner($field, showSpinner);
const message = getNotificationMessage($field);
if (message && type === ATTRIBUTE_NOTIFICATION_ERROR) {
appendErrorMessages($field, message);
return;
}
// the first call of this method is executed on init with jQuery wrapped object
// subsequent ones are the ones we care about and those are executed with DOM objects
if (!isJqueryObject(field) && !field.hasAttribute(ATTRIBUTE_NOTIFICATION_ERROR)) {
$field.parent().find('.error').remove();
}
}
function updateAriaInfo($field) {
const labels = [];
const id = $field.attr('id') || $field.attr('name');
const type = getFieldNotificationType($field);
const message = getNotificationMessage($field);
if (message && type === ATTRIBUTE_NOTIFICATION_ERROR) {
labels.push(`${id}-${ARIA_ERROR_POSTFIX}`);
}
if (getMessageContainer($field, 'description').length) {
labels.push(`${id}-${ARIA_DESCRIPTION_POSTFIX}`);
}
const $ariaTarget = conditionallyGetFieldTarget($field);
$ariaTarget.attr(ARIA_INFO_ATTRIBUTE, !!labels.length ? labels.join(' ') : null);
}
/**
* During the process of improving A11Y in the project,
* we discovered that for some form elements we need to
* make a shift of the target where aria-* attributes
* are to be applied.
*
* This function contains the mapping and returns
* the desired target, if it was found.
*
* If not - the same $field is returned back.
*/
function conditionallyGetFieldTarget($field) {
const modifiers = {
'aui-select': 'input[type="text"][role="combobox"]',
};
for (let [source, selector] of Object.entries(modifiers)) {
if ($field.is(source)) {
const $target = $field.find(selector);
if ($target.length) {
return $target;
}
}
}
return $field;
}
function isJqueryObject(el) {
return el.constructor.prototype.hasOwnProperty('jquery');
}
function errorMessageTemplate($field, messages) {
const id = $field.attr('id') || $field.attr('name');
const list = messages
.map(
(message) =>
`<li><span class="aui-icon aui-icon-small aui-iconfont-error aui-icon-notification"></span>${message}</li>`
)
.join('');
return `<div class="error" role="alert" id="${id}-${ARIA_ERROR_POSTFIX}"><ul>${list}</ul></div>`;
}
function descriptionTemplate($field, messages) {
const id = $field.attr('id') || $field.attr('name');
if (messages.length > 1) {
let list = messages.map((message) => `<li>${message}</li>`).join('');
return `<div class="description" id="${id}-${ARIA_DESCRIPTION_POSTFIX}"><ul>${list}</ul></div>`;
}
return `<div class="description" id="${id}-${ARIA_DESCRIPTION_POSTFIX}">${messages}</div>`;
}
function appendErrorMessages($field, messages) {
let previousErrors = getMessageContainer($field, 'error');
if (previousErrors.length > 0) {
previousErrors.remove();
}
$field.after(errorMessageTemplate($field, messages));
updateAriaInfo($field);
}
function getMessageContainer($field, type) {
return $field.parent().find(`.${type}`);
}
function isSpinnerForFieldAlreadyExisting($field) {
return $field.next('aui-spinner').length > 0;
}
function setFieldSpinner($field, isSpinnerVisible) {
if (isSpinnerVisible && !isSpinnerForFieldAlreadyExisting($field)) {
$field.after('<aui-spinner class="form-notification-spinner" size="small"></aui-spinner>');
} else {
$field.parent().find('aui-spinner').remove();
}
}
const deprecationLogger = getMessageLogger('data-aui-notification-field attribute', {
deprecationType: 'ATTRIBUTE',
alternativeName: 'HTML markup',
});
skate('data-aui-notification-field', {
attached: function (element) {
deprecationLogger();
initialiseNotification($(element));
},
attributes: (function () {
const attrs = {};
NOTIFICATION_PRIORITY.forEach(function (type) {
attrs[type] = synchroniseNotificationDisplay;
});
return attrs;
})(),
type: skate.type.ATTRIBUTE,
});
amdify('aui/form-notification');
export {
getMessageContainer,
appendErrorMessages,
appendDescription,
updateAriaInfo,
errorMessageTemplate,
setFieldSpinner,
};