Skip to content

Commit 7904d3e

Browse files
authored
refactor!: set focus-ring attribute on programmatic focus (#10049)
1 parent cfbc2f3 commit 7904d3e

File tree

56 files changed

+234
-105
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+234
-105
lines changed

packages/a11y-base/src/delegate-focus-mixin.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,24 @@ export const DelegateFocusMixin = dedupeMixin(
7373
if (this.autofocus && !this.disabled) {
7474
requestAnimationFrame(() => {
7575
this.focus();
76-
this.setAttribute('focus-ring', '');
7776
});
7877
}
7978
}
8079

8180
/**
81+
* @param {FocusOptions=} options
8282
* @protected
8383
* @override
8484
*/
85-
focus() {
85+
focus(options) {
8686
if (this.focusElement && !this.disabled) {
8787
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+
}
8894
}
8995
}
9096

packages/a11y-base/src/focus-mixin.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,21 @@ export const FocusMixin = dedupeMixin(
5454
}
5555
}
5656

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+
5772
/**
5873
* Override to change how focused and focus-ring attributes are set.
5974
*

packages/a11y-base/src/keyboard-direction-mixin.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ export declare class KeyboardDirectionMixinClass {
3232
/**
3333
* Focus the item at given index. Override this method to add custom logic.
3434
*/
35-
protected _focus(index: number, navigating: boolean): void;
35+
protected _focus(index: number, options: FocusOptions, navigating: boolean): void;
3636

3737
/**
3838
* Focus the given item. Override this method to add custom logic.
3939
*/
40-
protected _focusItem(item: Element, navigating: boolean): void;
40+
protected _focusItem(item: Element, options: FocusOptions, navigating: boolean): void;
4141

4242
/**
4343
* Returns whether the item is focusable. By default,

packages/a11y-base/src/keyboard-direction-mixin.js

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,17 @@ export const KeyboardDirectionMixin = (superclass) =>
3838
return false;
3939
}
4040

41-
/** @protected */
42-
focus() {
41+
/**
42+
* @param {FocusOptions=} options
43+
* @protected
44+
* @override
45+
*/
46+
focus(options) {
4347
const items = this._getItems();
4448
if (Array.isArray(items)) {
4549
const idx = this._getAvailableIndex(items, 0, null, (item) => !isElementHidden(item));
4650
if (idx >= 0) {
47-
this._focus(idx);
51+
this._focus(idx, options);
4852
}
4953
}
5054
}
@@ -114,7 +118,7 @@ export const KeyboardDirectionMixin = (superclass) =>
114118

115119
if (idx >= 0) {
116120
event.preventDefault();
117-
this._focus(idx, true);
121+
this._focus(idx, { focusVisible: true }, true);
118122
}
119123
}
120124

@@ -150,30 +154,27 @@ export const KeyboardDirectionMixin = (superclass) =>
150154
* Focus the item at given index. Override this method to add custom logic.
151155
*
152156
* @param {number} index
157+
* @param {FocusOptions=} options
153158
* @param {boolean} navigating
154159
* @protected
155160
*/
156-
_focus(index, navigating = false) {
161+
_focus(index, options, navigating = false) {
157162
const items = this._getItems();
158163

159-
this._focusItem(items[index], navigating);
164+
this._focusItem(items[index], options, navigating);
160165
}
161166

162167
/**
163168
* Focus the given item. Override this method to add custom logic.
164169
*
165170
* @param {Element} item
171+
* @param {FocusOptions=} options
166172
* @param {boolean} navigating
167173
* @protected
168174
*/
169-
_focusItem(item) {
175+
_focusItem(item, options) {
170176
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', '');
177+
item.focus(options);
177178
}
178179
}
179180

packages/a11y-base/src/list-mixin.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,12 @@ export const ListMixin = (superClass) =>
107107
return this.orientation !== 'horizontal';
108108
}
109109

110-
focus() {
110+
/**
111+
* @param {FocusOptions=} options
112+
* @protected
113+
* @override
114+
*/
115+
focus(options) {
111116
// In initialization (e.g vaadin-select) observer might not been run yet.
112117
if (this._observer) {
113118
this._observer.flush();
@@ -116,10 +121,10 @@ export const ListMixin = (superClass) =>
116121
const items = Array.isArray(this.items) ? this.items : [];
117122
const idx = this._getAvailableIndex(items, 0, null, (item) => item.tabIndex === 0 && !isElementHidden(item));
118123
if (idx >= 0) {
119-
this._focus(idx);
124+
this._focus(idx, options);
120125
} else {
121126
// Call `KeyboardDirectionMixin` logic to focus first non-disabled item.
122-
super.focus();
127+
super.focus(options);
123128
}
124129
}
125130

@@ -275,13 +280,13 @@ export const ListMixin = (superClass) =>
275280
* @param {number} idx
276281
* @protected
277282
*/
278-
_focus(idx) {
283+
_focus(idx, options) {
279284
this.items.forEach((e, index) => {
280285
e.focused = index === idx;
281286
});
282287
this._setFocusable(idx);
283288
this._scrollToItem(idx);
284-
super._focus(idx);
289+
super._focus(idx, options);
285290
}
286291

287292
/**

packages/a11y-base/src/tabindex-mixin.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,13 @@ export const TabindexMixin = (superclass) =>
9595
* `tabindex` to -1 does not prevent the element from being
9696
* programmatically focusable.
9797
*
98+
* @param {FocusOptions=} options
9899
* @protected
99100
* @override
100101
*/
101-
focus() {
102+
focus(options) {
102103
if (!this.disabled || this.__shouldAllowFocusWhenDisabled()) {
103-
super.focus();
104+
super.focus(options);
104105
}
105106
}
106107

packages/a11y-base/test/delegate-focus-mixin.test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ describe('DelegateFocusMixin', () => {
8282
expect(element.hasAttribute('focused')).to.be.false;
8383
});
8484

85+
it('should set focus-ring attribute when calling focus() by default', () => {
86+
element.focus();
87+
expect(element.hasAttribute('focus-ring')).to.be.true;
88+
});
89+
90+
it('should not set focus-ring attribute when calling focus() with focusVisible: false', () => {
91+
element.focus({ focusVisible: false });
92+
expect(element.hasAttribute('focus-ring')).to.be.false;
93+
});
94+
8595
it('should propagate disabled property to the input', () => {
8696
element.disabled = true;
8797
expect(input.disabled).to.be.true;

packages/a11y-base/test/focus-mixin.test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,14 @@ describe('FocusMixin', () => {
5656
focusin(input);
5757
expect(element.hasAttribute('focus-ring')).to.be.false;
5858
});
59+
60+
it('should set focus-ring attribute when calling focus() by default', () => {
61+
element.focus();
62+
expect(element.hasAttribute('focus-ring')).to.be.true;
63+
});
64+
65+
it('should not set focus-ring attribute when calling focus() with focusVisible: false', () => {
66+
element.focus({ focusVisible: false });
67+
expect(element.hasAttribute('focus-ring')).to.be.false;
68+
});
5969
});

packages/a11y-base/test/keyboard-direction-mixin.test.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,6 @@ describe('KeyboardDirectionMixin', () => {
177177
expect(element.focused).to.be.equal(items[4]);
178178
});
179179

180-
it('should set focus-ring on the focused element on keydown', () => {
181-
arrowDownKeyDown(items[0]);
182-
expect(items[1].hasAttribute('focus-ring')).to.be.true;
183-
});
184-
185180
it('should not move focus on keydown with Ctrl key modifier', () => {
186181
const spy = sinon.spy(items[1], 'focus');
187182
arrowDownKeyDown(items[0], ['ctrl']);

packages/a11y-base/test/list-mixin.test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,15 @@ describe('ListMixin', () => {
251251
expect(spy.calledOnce).to.be.true;
252252
});
253253

254+
it('should pass focus options to the item when calling focus() on the list', async () => {
255+
list._setFocusable(3);
256+
await nextUpdate(list);
257+
const spy = sinon.spy(list.items[3], 'focus');
258+
list.focus({ focusVisible: false });
259+
expect(spy.calledOnce).to.be.true;
260+
expect(spy.firstCall.args[0]).to.deep.equal({ focusVisible: false });
261+
});
262+
254263
it('should call focus() on the first non-disabled item if all items have tabindex -1', () => {
255264
list.items.forEach((item) => {
256265
item.tabIndex = -1;

0 commit comments

Comments
 (0)