@vaadin/a11y-base 25.0.0-alpha9 → 25.0.0-beta2

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/a11y-base",
3
- "version": "25.0.0-alpha9",
3
+ "version": "25.0.0-beta2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -31,14 +31,14 @@
31
31
  ],
32
32
  "dependencies": {
33
33
  "@open-wc/dedupe-mixin": "^1.3.0",
34
- "@vaadin/component-base": "25.0.0-alpha9",
34
+ "@vaadin/component-base": "25.0.0-beta2",
35
35
  "lit": "^3.0.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@vaadin/chai-plugins": "25.0.0-alpha9",
39
- "@vaadin/test-runner-commands": "25.0.0-alpha9",
38
+ "@vaadin/chai-plugins": "25.0.0-beta2",
39
+ "@vaadin/test-runner-commands": "25.0.0-beta2",
40
40
  "@vaadin/testing-helpers": "^2.0.0",
41
- "sinon": "^18.0.0"
41
+ "sinon": "^21.0.0"
42
42
  },
43
- "gitHead": "bbe4720721e0955ffc87a79b412bee38b1f0eb1e"
43
+ "gitHead": "e078f8371ae266f05c7ca1ec25686cc489c83f24"
44
44
  }
@@ -115,7 +115,7 @@ const applyAttributeToOthers = (originalTarget, parentNode, markerName, controlA
115
115
  targets.forEach(keep);
116
116
 
117
117
  /**
118
- * @param {?Node} el
118
+ * @param {?Node} parent
119
119
  */
120
120
  const deep = (parent) => {
121
121
  if (!parent || elementsToStop.has(parent)) {
@@ -17,7 +17,7 @@ const attributeToTargets = new Map();
17
17
  *
18
18
  * @param {string} attr the attribute name used as key in the map
19
19
  *
20
- * @returns {WeakMap<HTMLElement, Set<string>} a weak map with the stored values for the elements being controlled by the helper
20
+ * @return {WeakMap<HTMLElement, Set<string>>} a weak map with the stored values for the elements being controlled by the helper
21
21
  */
22
22
  function getAttrMap(attr) {
23
23
  if (!attributeToTargets.has(attr)) {
@@ -32,8 +32,6 @@ function getAttrMap(attr) {
32
32
  *
33
33
  * @param {HTMLElement} target
34
34
  * @param {string} attr the attribute to be cleared
35
- * @param {boolean} storeValue whether or not the current value of the attribute should be stored on the map
36
- * @returns
37
35
  */
38
36
  function cleanAriaIDReference(target, attr) {
39
37
  if (!target) {
@@ -73,18 +73,24 @@ export const DelegateFocusMixin = dedupeMixin(
73
73
  if (this.autofocus && !this.disabled) {
74
74
  requestAnimationFrame(() => {
75
75
  this.focus();
76
- this.setAttribute('focus-ring', '');
77
76
  });
78
77
  }
79
78
  }
80
79
 
81
80
  /**
81
+ * @param {FocusOptions=} options
82
82
  * @protected
83
83
  * @override
84
84
  */
85
- focus() {
85
+ focus(options) {
86
86
  if (this.focusElement && !this.disabled) {
87
87
  this.focusElement.focus();
88
+
89
+ // Set focus-ring attribute on programmatic focus by default
90
+ // unless explicitly disabled by `{ focusVisible: false }`.
91
+ if (!(options && options.focusVisible === false)) {
92
+ this.setAttribute('focus-ring', '');
93
+ }
88
94
  }
89
95
  }
90
96
 
@@ -54,6 +54,21 @@ export const FocusMixin = dedupeMixin(
54
54
  }
55
55
  }
56
56
 
57
+ /**
58
+ * @param {FocusOptions=} options
59
+ * @protected
60
+ * @override
61
+ */
62
+ focus(options) {
63
+ super.focus(options);
64
+
65
+ // Set focus-ring attribute on programmatic focus by default
66
+ // unless explicitly disabled by `{ focusVisible: false }`.
67
+ if (!(options && options.focusVisible === false)) {
68
+ this.setAttribute('focus-ring', '');
69
+ }
70
+ }
71
+
57
72
  /**
58
73
  * Override to change how focused and focus-ring attributes are set.
59
74
  *
@@ -29,16 +29,19 @@ export class FocusRestorationController {
29
29
  return;
30
30
  }
31
31
 
32
- const preventScroll = options ? options.preventScroll : false;
32
+ const focusOptions = {
33
+ preventScroll: options ? options.preventScroll : false,
34
+ focusVisible: options ? options.focusVisible : false,
35
+ };
33
36
 
34
37
  if (getDeepActiveElement() === document.body) {
35
38
  // In Firefox and Safari, focusing the node synchronously
36
39
  // doesn't work as expected when the overlay is closing on outside click.
37
40
  // These browsers force focus to move to the body element and retain it
38
41
  // there until the next event loop iteration.
39
- setTimeout(() => focusNode.focus({ preventScroll }));
42
+ setTimeout(() => focusNode.focus(focusOptions));
40
43
  } else {
41
- focusNode.focus({ preventScroll });
44
+ focusNode.focus(focusOptions);
42
45
  }
43
46
 
44
47
  this.focusNode = null;
@@ -3,7 +3,7 @@
3
3
  * Copyright (c) 2021 - 2025 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
- import { getFocusableElements, isElementFocused } from './focus-utils.js';
6
+ import { getFocusableElements, isElementFocused, isKeyboardActive } from './focus-utils.js';
7
7
 
8
8
  const instances = [];
9
9
 
@@ -87,7 +87,7 @@ export class FocusTrapController {
87
87
  instances.push(this);
88
88
 
89
89
  if (this.__focusedElementIndex === -1) {
90
- this.__focusableElements[0].focus();
90
+ this.__focusableElements[0].focus({ focusVisible: isKeyboardActive() });
91
91
  }
92
92
  }
93
93
 
@@ -147,7 +147,7 @@ export class FocusTrapController {
147
147
  const currentIndex = this.__focusedElementIndex;
148
148
  const nextIndex = (focusableElements.length + currentIndex + step) % focusableElements.length;
149
149
  const element = focusableElements[nextIndex];
150
- element.focus();
150
+ element.focus({ focusVisible: true });
151
151
  if (element.localName === 'input') {
152
152
  element.select();
153
153
  }
@@ -32,12 +32,12 @@ export declare class KeyboardDirectionMixinClass {
32
32
  /**
33
33
  * Focus the item at given index. Override this method to add custom logic.
34
34
  */
35
- protected _focus(index: number, navigating: boolean): void;
35
+ protected _focus(index: number, options: FocusOptions, navigating: boolean): void;
36
36
 
37
37
  /**
38
38
  * Focus the given item. Override this method to add custom logic.
39
39
  */
40
- protected _focusItem(item: Element, navigating: boolean): void;
40
+ protected _focusItem(item: Element, options: FocusOptions, navigating: boolean): void;
41
41
 
42
42
  /**
43
43
  * Returns whether the item is focusable. By default,
@@ -38,17 +38,29 @@ export const KeyboardDirectionMixin = (superclass) =>
38
38
  return false;
39
39
  }
40
40
 
41
- /** @protected */
42
- focus() {
43
- const items = this._getItems();
44
- if (Array.isArray(items)) {
45
- const idx = this._getAvailableIndex(items, 0, null, (item) => !isElementHidden(item));
46
- if (idx >= 0) {
47
- this._focus(idx);
48
- }
41
+ /**
42
+ * @param {FocusOptions=} options
43
+ * @protected
44
+ * @override
45
+ */
46
+ focus(options) {
47
+ const idx = this._getFocusableIndex();
48
+ if (idx >= 0) {
49
+ this._focus(idx, options);
49
50
  }
50
51
  }
51
52
 
53
+ /**
54
+ * Get the index of a first focusable item, if any.
55
+ *
56
+ * @return {Element[]}
57
+ * @protected
58
+ */
59
+ _getFocusableIndex() {
60
+ const items = this._getItems();
61
+ return Array.isArray(items) ? this._getAvailableIndex(items, 0, null, (item) => !isElementHidden(item)) : -1;
62
+ }
63
+
52
64
  /**
53
65
  * Get the list of items participating in keyboard navigation.
54
66
  * By default, it treats all the light DOM children as items.
@@ -104,17 +116,18 @@ export const KeyboardDirectionMixin = (superclass) =>
104
116
  if (
105
117
  this._tabNavigation &&
106
118
  key === 'Tab' &&
107
- ((idx > currentIdx && event.shiftKey) || (idx < currentIdx && !event.shiftKey))
119
+ ((idx > currentIdx && event.shiftKey) || (idx < currentIdx && !event.shiftKey) || idx === currentIdx)
108
120
  ) {
109
121
  // Prevent "roving tabindex" logic and let the normal Tab behavior if
110
122
  // - currently on the first focusable item and Shift + Tab is pressed,
111
- // - currently on the last focusable item and Tab is pressed.
123
+ // - currently on the last focusable item and Tab is pressed,
124
+ // - currently on the only focusable item and Tab is pressed
112
125
  return;
113
126
  }
114
127
 
115
128
  if (idx >= 0) {
116
129
  event.preventDefault();
117
- this._focus(idx, true);
130
+ this._focus(idx, { focusVisible: true }, true);
118
131
  }
119
132
  }
120
133
 
@@ -150,30 +163,27 @@ export const KeyboardDirectionMixin = (superclass) =>
150
163
  * Focus the item at given index. Override this method to add custom logic.
151
164
  *
152
165
  * @param {number} index
166
+ * @param {FocusOptions=} options
153
167
  * @param {boolean} navigating
154
168
  * @protected
155
169
  */
156
- _focus(index, navigating = false) {
170
+ _focus(index, options, navigating = false) {
157
171
  const items = this._getItems();
158
172
 
159
- this._focusItem(items[index], navigating);
173
+ this._focusItem(items[index], options, navigating);
160
174
  }
161
175
 
162
176
  /**
163
177
  * Focus the given item. Override this method to add custom logic.
164
178
  *
165
179
  * @param {Element} item
180
+ * @param {FocusOptions=} options
166
181
  * @param {boolean} navigating
167
182
  * @protected
168
183
  */
169
- _focusItem(item) {
184
+ _focusItem(item, options) {
170
185
  if (item) {
171
- item.focus();
172
-
173
- // Generally, the items are expected to implement `FocusMixin`
174
- // that would set this attribute based on the `keydown` event.
175
- // We set it manually to handle programmatic focus() calls.
176
- item.setAttribute('focus-ring', '');
186
+ item.focus(options);
177
187
  }
178
188
  }
179
189
 
@@ -36,15 +36,9 @@ export declare class ListMixinClass {
36
36
  orientation: 'horizontal' | 'vertical';
37
37
 
38
38
  /**
39
- * The list of items from which a selection can be made.
39
+ * A read-only list of items from which a selection can be made.
40
40
  * It is populated from the elements passed to the light DOM,
41
41
  * and updated dynamically when adding or removing items.
42
- *
43
- * The item elements must implement `Vaadin.ItemMixin`.
44
- *
45
- * Note: unlike `<vaadin-combo-box>`, this property is read-only,
46
- * so if you want to provide items by iterating array of data,
47
- * you have to use `dom-repeat` and place it to the light DOM.
48
42
  */
49
43
  readonly items: Element[] | undefined;
50
44
 
package/src/list-mixin.js CHANGED
@@ -6,7 +6,6 @@
6
6
  import { timeOut } from '@vaadin/component-base/src/async.js';
7
7
  import { Debouncer } from '@vaadin/component-base/src/debounce.js';
8
8
  import { getNormalizedScrollLeft, setNormalizedScrollLeft } from '@vaadin/component-base/src/dir-utils.js';
9
- import { getFlattenedElements } from '@vaadin/component-base/src/dom-utils.js';
10
9
  import { SlotObserver } from '@vaadin/component-base/src/slot-observer.js';
11
10
  import { isElementHidden } from './focus-utils.js';
12
11
  import { KeyboardDirectionMixin } from './keyboard-direction-mixin.js';
@@ -56,15 +55,9 @@ export const ListMixin = (superClass) =>
56
55
  },
57
56
 
58
57
  /**
59
- * The list of items from which a selection can be made.
58
+ * A read-only list of items from which a selection can be made.
60
59
  * It is populated from the elements passed to the light DOM,
61
60
  * and updated dynamically when adding or removing items.
62
- *
63
- * The item elements must implement `Vaadin.ItemMixin`.
64
- *
65
- * Note: unlike `<vaadin-combo-box>`, this property is read-only,
66
- * so if you want to provide items by iterating array of data,
67
- * you have to use `dom-repeat` and place it to the light DOM.
68
61
  * @type {!Array<!Element> | undefined}
69
62
  */
70
63
  items: {
@@ -114,7 +107,12 @@ export const ListMixin = (superClass) =>
114
107
  return this.orientation !== 'horizontal';
115
108
  }
116
109
 
117
- focus() {
110
+ /**
111
+ * @param {FocusOptions=} options
112
+ * @protected
113
+ * @override
114
+ */
115
+ focus(options) {
118
116
  // In initialization (e.g vaadin-select) observer might not been run yet.
119
117
  if (this._observer) {
120
118
  this._observer.flush();
@@ -123,10 +121,10 @@ export const ListMixin = (superClass) =>
123
121
  const items = Array.isArray(this.items) ? this.items : [];
124
122
  const idx = this._getAvailableIndex(items, 0, null, (item) => item.tabIndex === 0 && !isElementHidden(item));
125
123
  if (idx >= 0) {
126
- this._focus(idx);
124
+ this._focus(idx, options);
127
125
  } else {
128
126
  // Call `KeyboardDirectionMixin` logic to focus first non-disabled item.
129
- super.focus();
127
+ super.focus(options);
130
128
  }
131
129
  }
132
130
 
@@ -138,7 +136,7 @@ export const ListMixin = (superClass) =>
138
136
 
139
137
  const slot = this.shadowRoot.querySelector('slot:not([name])');
140
138
  this._observer = new SlotObserver(slot, () => {
141
- this._setItems(this._filterItems(getFlattenedElements(this)));
139
+ this._setItems(this._filterItems([...this.children]));
142
140
  });
143
141
  }
144
142
 
@@ -282,13 +280,13 @@ export const ListMixin = (superClass) =>
282
280
  * @param {number} idx
283
281
  * @protected
284
282
  */
285
- _focus(idx) {
283
+ _focus(idx, options) {
286
284
  this.items.forEach((e, index) => {
287
285
  e.focused = index === idx;
288
286
  });
289
287
  this._setFocusable(idx);
290
288
  this._scrollToItem(idx);
291
- super._focus(idx);
289
+ super._focus(idx, options);
292
290
  }
293
291
 
294
292
  /**
@@ -95,12 +95,13 @@ export const TabindexMixin = (superclass) =>
95
95
  * `tabindex` to -1 does not prevent the element from being
96
96
  * programmatically focusable.
97
97
  *
98
+ * @param {FocusOptions=} options
98
99
  * @protected
99
100
  * @override
100
101
  */
101
- focus() {
102
+ focus(options) {
102
103
  if (!this.disabled || this.__shouldAllowFocusWhenDisabled()) {
103
- super.focus();
104
+ super.focus(options);
104
105
  }
105
106
  }
106
107