@atlassian/aui 9.7.1 → 9.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@atlassian/aui",
3
3
  "description": "Atlassian User Interface library",
4
- "version": "9.7.1",
4
+ "version": "9.7.2",
5
5
  "author": "Atlassian Pty Ltd.",
6
6
  "homepage": "https://aui.atlassian.com",
7
7
  "license": "Apache-2.0",
@@ -1,37 +1,30 @@
1
1
  import skate from './internal/skate';
2
2
 
3
- const DEFAULT_VALUE = 'bottom-end'
3
+
4
+ const DEFAULT_PLACEMENT_VALUE = 'bottom-end';
5
+ const PLACEMENT_OPTIONS = ['top-start', 'top-end', 'bottom-start', 'bottom-end'];
4
6
 
5
7
  const setBadgedPlacement = (element, newValue, oldValue) => {
6
- const placementOptions = ['top-start', 'top-end', 'bottom-start', 'bottom-end'];
7
- const newPlacementClass = placementOptions.includes(newValue) ? newValue : DEFAULT_VALUE;
8
+ const newPlacement = PLACEMENT_OPTIONS.includes(newValue) ? newValue : DEFAULT_PLACEMENT_VALUE;
8
9
 
9
- if (oldValue !== null) {
10
+ if (oldValue) {
10
11
  element.classList.remove(`aui-avatar-badged-${oldValue}`);
11
12
  }
12
- element.classList.add(`aui-avatar-badged-${newPlacementClass}`);
13
+ element.classList.add(`aui-avatar-badged-${newPlacement}`);
13
14
  }
14
15
 
15
16
  const AvatarBadged = skate('aui-avatar-badged', {
16
- attached(element) {
17
- const value = element.getAttribute('placement');
18
- if (value.length) {
19
- setBadgedPlacement(element, value);
20
- }
21
- },
22
-
23
17
  attributes: {
24
18
  placement: {
25
- value: DEFAULT_VALUE,
26
-
27
- fallback: function(element, { newValue, oldValue}) {
19
+ value: DEFAULT_PLACEMENT_VALUE,
20
+ fallback(element, { newValue, oldValue}) {
28
21
  setBadgedPlacement(element, newValue, oldValue);
29
22
  },
30
- }
23
+ },
31
24
  },
32
25
 
33
26
  created: function(element) {
34
- element.className = 'aui-avatar-badged';
27
+ element.classList.add('aui-avatar-badged');
35
28
  }
36
29
  });
37
30
 
@@ -2,14 +2,16 @@ import skate from './internal/skate';
2
2
 
3
3
  const DEFAULT_SIZE = 'medium';
4
4
  const sizes = {xsmall: 16, small: 24, medium: 32, large: 48, xlarge: 64, xxlarge: 96, xxxlarge: 128};
5
- let buttonDropdownIsActive = false;
5
+ let isButtonDropdownActive = false;
6
6
 
7
- const setDefaultSizeValue = (value) => {
7
+ const getAvatarGroupSize = (value) => {
8
8
  return Object.keys(sizes).includes(value) ? value : DEFAULT_SIZE;
9
9
  }
10
10
 
11
11
  const setAvatarGroupSize = (element, newValue, oldValue) => {
12
- element.classList.remove(`aui-avatar-group-${oldValue}`);
12
+ if (oldValue) {
13
+ element.classList.remove(`aui-avatar-group-${oldValue}`);
14
+ }
13
15
  element.classList.add(`aui-avatar-group-${newValue}`);
14
16
  }
15
17
 
@@ -29,9 +31,7 @@ const setBadgedDropdownItem = (element) => {
29
31
  const badgedDropdown = element.parentNode.querySelector('.aui-avatar-group-dropdown')
30
32
  const hiddenAvatarsList = [... avatars].slice(4);
31
33
 
32
- if (!buttonDropdownIsActive) {
33
- buttonDropdownIsActive = true;
34
-
34
+ if (isButtonDropdownActive) {
35
35
  hiddenAvatarsList.forEach(avatar => {
36
36
  const avatarItemSrc = avatar.getAttribute('src');
37
37
  const avatarItemAlt = avatar.getAttribute('alt');
@@ -49,8 +49,6 @@ const setBadgedDropdownItem = (element) => {
49
49
  badgedDropdown.appendChild(avatarWrapper);
50
50
  });
51
51
  } else {
52
- buttonDropdownIsActive = false;
53
-
54
52
  hiddenAvatarsList.forEach(()=> {
55
53
  const dropdownItems = document.querySelector('.aui-avatar-group-dropdown-item');
56
54
  if (dropdownItems) {dropdownItems.remove();}
@@ -60,8 +58,11 @@ const setBadgedDropdownItem = (element) => {
60
58
  }
61
59
 
62
60
  const handleClickOnBadged = (element) => {
63
- const badgedDropdown = element.parentNode.querySelector('.aui-avatar-group-dropdown')
61
+ const badgedDropdown = element.parentNode.querySelector('.aui-avatar-group-dropdown');
64
62
  badgedDropdown.classList.toggle('aui-avatar-group-dropdown-show');
63
+
64
+ isButtonDropdownActive = !isButtonDropdownActive;
65
+ element.setAttribute('aria-expanded', isButtonDropdownActive.toString());
65
66
 
66
67
  setBadgedDropdownItem(element);
67
68
  }
@@ -78,6 +79,8 @@ const createAvatarGroupBadged = (element, size, avatars, amount) => {
78
79
  groupBadged.classList.add('aui-avatar-group-badged', 'aui-avatar-inner');
79
80
  groupBadged.style.left = setLeftPosition(size, 4.5, multiplier);
80
81
  groupBadged.innerText = `+${amount}`;
82
+ groupBadged.setAttribute('aria-expanded', isButtonDropdownActive.toString());
83
+ groupBadged.setAttribute('aria-haspopup', 'dialog')
81
84
  element.appendChild(groupBadged);
82
85
 
83
86
  groupBadged.addEventListener('click', () => handleClickOnBadged(groupBadged))
@@ -111,21 +114,17 @@ const AvatarGroupEl = skate('aui-avatar-group', {
111
114
  value: DEFAULT_SIZE,
112
115
 
113
116
  fallback(element, { newValue , oldValue }) {
114
- let value = setDefaultSizeValue(newValue);
117
+ const size = getAvatarGroupSize(newValue);
115
118
 
116
119
  skate.init(element);
117
- setAvatarGroupSize(element, value, oldValue);
118
- tidyUpAvatarGroup(element, value);
120
+ setAvatarGroupSize(element, size, oldValue);
121
+ tidyUpAvatarGroup(element, size);
119
122
  }
120
123
  }
121
124
  },
122
125
 
123
126
  created(element) {
124
- const avatarGroupSize = element.getAttribute('size')
125
- const value = setDefaultSizeValue(avatarGroupSize);
126
-
127
- element.className = 'aui-avatar-group';
128
- setAvatarGroupSize(element, value);
127
+ element.classList.add('aui-avatar-group');
129
128
  }
130
129
  });
131
130
 
@@ -9,6 +9,7 @@ const DEFAULT_AVATAR_IMAGES = new Map([
9
9
  ]);
10
10
 
11
11
  const getElementWrapper = (element, elementFind) => element.querySelector(elementFind);
12
+ const containsBadge = (element) => element.querySelector('aui-avatar-badged') !== null;
12
13
 
13
14
  const replaceClass = (element, newValue, oldValue) => {
14
15
  const elementWrapper = getElementWrapper(element, '.aui-avatar');
@@ -48,20 +49,19 @@ const AvatarEl = skate('aui-avatar', {
48
49
  size(element, { newValue , oldValue}) {
49
50
  replaceClass(element, newValue, oldValue);
50
51
  },
51
-
52
52
  type(element, { newValue, oldValue}) {
53
53
  replaceClass(element, newValue, oldValue);
54
54
  setDefaultAvatarImage(element);
55
55
  },
56
-
57
56
  src(element, { newValue: value }) {
58
57
  setImageSrc(element, value);
59
58
  },
60
-
61
- alt(element, {newValue: value}) {
62
- setImageAlt(element, value);
59
+ alt: {
60
+ value: '',
61
+ fallback(element, {newValue: value}) {
62
+ setImageAlt(element, value);
63
+ }
63
64
  },
64
-
65
65
  title(element, {newValue: value}) {
66
66
  setImageTitle(element, value);
67
67
  },
@@ -70,6 +70,10 @@ const AvatarEl = skate('aui-avatar', {
70
70
  created: function(element) {
71
71
  element.className = 'aui-avatar';
72
72
  setDefaultAvatarImage(element);
73
+
74
+ if (containsBadge(element)) {
75
+ element.setAttribute('role', 'img')
76
+ }
73
77
  }
74
78
  });
75
79
 
@@ -119,7 +119,38 @@ function updateAriaInfo($field) {
119
119
  labels.push(`${id}-${ARIA_DESCRIPTION_POSTFIX}`);
120
120
  }
121
121
 
122
- $field.attr(ARIA_INFO_ATTRIBUTE, !!labels.length ? labels.join(' ') : null);
122
+ const $ariaTarget = conditionallyGetFieldTarget($field);
123
+
124
+ $ariaTarget.attr(ARIA_INFO_ATTRIBUTE, !!labels.length ? labels.join(' ') : null);
125
+ }
126
+
127
+ /**
128
+ * During the process of improving A11Y in the project,
129
+ * we discovered that for some form elements we need to
130
+ * make a shift of the target where aria-* attributes
131
+ * are to be applied.
132
+ *
133
+ * This function contains the mapping and returns
134
+ * the desired target, if it was found.
135
+ *
136
+ * If not - the same $field is returned back.
137
+ */
138
+ function conditionallyGetFieldTarget($field) {
139
+ const modifiers = {
140
+ 'aui-select': 'input[type="text"][role="combobox"]'
141
+ };
142
+
143
+ for (let [source, selector] of Object.entries(modifiers)) {
144
+ if ($field.is(source)) {
145
+ const $target = $field.find(selector);
146
+
147
+ if ($target.length) {
148
+ return $target;
149
+ }
150
+ }
151
+ }
152
+
153
+ return $field;
123
154
  }
124
155
 
125
156
  function isJqueryObject(el) {
@@ -132,7 +163,7 @@ function errorMessageTemplate($field, messages) {
132
163
  .map(message => `<li><span class="aui-icon aui-icon-small aui-iconfont-error aui-icon-notification"></span>${message}</li>`)
133
164
  .join('');
134
165
 
135
- return `<div class="error" id="${id}-${ARIA_ERROR_POSTFIX}"><ul>${list}</ul></div>`;
166
+ return `<div class="error" role="alert" id="${id}-${ARIA_ERROR_POSTFIX}"><ul>${list}</ul></div>`;
136
167
  }
137
168
 
138
169
  function descriptionTemplate($field, messages) {
@@ -183,6 +183,7 @@ function createArgumentAccessorFunction($field) {
183
183
 
184
184
  function changeFieldState($field, state, message) {
185
185
  $field.attr('data-' + ATTRIBUTE_FIELD_STATE, state);
186
+ $field.attr('aria-invalid', false);
186
187
 
187
188
  if (state === UNVALIDATED) {
188
189
  return;
@@ -205,6 +206,9 @@ function changeFieldState($field, state, message) {
205
206
  setFieldNotification($displayField, notificationType, message);
206
207
  }
207
208
 
209
+ if (state === INVALID) {
210
+ $field.attr('aria-invalid', true);
211
+ }
208
212
  }
209
213
 
210
214
  function showSpinnerIfSlow($field) {
@@ -39,34 +39,85 @@ $.fn.auiSelect2 = function (first) {
39
39
  updatedArgs = arguments;
40
40
  }
41
41
 
42
+ const options = updatedArgs[0];
43
+
44
+ /**
45
+ * AUI-5464: after the upgrade to select2 v3.5.4, custom error handling stopped
46
+ * working, as the `ajax.params.error` function is overriden by select2, and
47
+ * there is no alternative to this...
48
+ *
49
+ * This is a workaround for creating an array of ajax error handlers that will
50
+ * contain the default handler and the custom ones, which is supported starting
51
+ * from jQuery v1.5: http://api.jquery.com/jquery.ajax/
52
+ *
53
+ * Please note this issue is fixed starting from select2 v4, though the data format
54
+ * is different.
55
+ *
56
+ * @see https://atlassian.slack.com/archives/CFGN5350T/p1686741137056489
57
+ */
58
+ if (options.ajax && options.ajax.params && options.ajax.params.error) {
59
+ let customErrorHandlers = options.ajax.params.error;
60
+ if (!Array.isArray(customErrorHandlers)) {
61
+ customErrorHandlers = [customErrorHandlers];
62
+ }
63
+ let originalTransport = options.ajax.transport;
64
+ if (!originalTransport) {
65
+ originalTransport = originalSelect2.ajaxDefaults.transport
66
+ }
67
+
68
+ const newTransport = function(...args) {
69
+ args[0].error = [args[0].error, ...customErrorHandlers];
70
+ return originalTransport(...args);
71
+ };
72
+
73
+ options.ajax.transport = newTransport;
74
+ }
75
+
42
76
  const result = originalSelect2.apply(this, updatedArgs);
43
77
  const select2Instance = this;
44
- const searchLabel = updatedArgs[0].searchLabel;
78
+ const searchLabel = options.searchLabel;
45
79
 
46
80
  select2Instance.on('select2-open', function () {
47
81
  const $selectInput = $(this);
48
82
 
49
- if (updatedArgs[0].multiple || $selectInput.attr('multiple')) {
83
+ if (options.multiple || $selectInput.attr('multiple')) {
50
84
  // This is a multi-select, exiting
51
85
  return;
52
86
  }
53
87
 
54
- const $selectDropdown = $selectInput.select2('dropdown')
88
+ const $selectDropdown = $selectInput.select2('dropdown');
55
89
 
56
90
  if (searchLabel) {
57
91
  $selectDropdown.find('.select2-search label').text(searchLabel);
58
92
  }
93
+ });
94
+
95
+ select2Instance.on('select2-close', function () {
96
+ const $selectInput = $(this);
97
+ $selectInput.removeData('was-ariadescribedby-cleared');
98
+ });
99
+
100
+ select2Instance.on('select2-loaded', function () {
101
+ const $selectInput = $(this);
102
+ const wasAriaDescribedByCleared = $selectInput.data('was-ariadescribedby-cleared');
103
+
104
+ if (options.multiple || $selectInput.attr('multiple') || wasAriaDescribedByCleared) {
105
+ return;
106
+ }
107
+
108
+ const $selectDropdown = $selectInput.select2('dropdown');
59
109
 
60
110
  // AUI-5461: when single select dropdown opens up, the first option is
61
111
  // instantly focused, making SRs announce it, while skipping the search field
62
112
  $selectDropdown.find('.select2-search .select2-input').attr('aria-activedescendant', '');
113
+ $selectInput.data('was-ariadescribedby-cleared', true);
63
114
  });
64
115
 
65
116
  select2Instance.on('select2-focus', function () {
66
117
  const $selectInput = $(this);
67
118
  const $container = $selectInput.parent().find('.select2-container');
68
119
 
69
- if (updatedArgs[0].multiple || $selectInput.attr('multiple')) {
120
+ if (options.multiple || $selectInput.attr('multiple')) {
70
121
  if (searchLabel) {
71
122
  $container.find('.select2-search-field label').text(searchLabel);
72
123
  }