Skip to content

Commit 40047fb

Browse files
authored
refactor!: set focus-ring attribute on programmatic focus (#10049) (#10126)
1 parent 0ab8e31 commit 40047fb

File tree

55 files changed

+213
-99
lines changed

Some content is hidden

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

55 files changed

+213
-99
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
@@ -113,7 +113,12 @@ export const ListMixin = (superClass) =>
113113
return this.orientation !== 'horizontal';
114114
}
115115

116-
focus() {
116+
/**
117+
* @param {FocusOptions=} options
118+
* @protected
119+
* @override
120+
*/
121+
focus(options) {
117122
// In initialization (e.g vaadin-select) observer might not been run yet.
118123
if (this._observer) {
119124
this._observer.flush();
@@ -122,10 +127,10 @@ export const ListMixin = (superClass) =>
122127
const items = Array.isArray(this.items) ? this.items : [];
123128
const idx = this._getAvailableIndex(items, 0, null, (item) => item.tabIndex === 0 && !isElementHidden(item));
124129
if (idx >= 0) {
125-
this._focus(idx);
130+
this._focus(idx, options);
126131
} else {
127132
// Call `KeyboardDirectionMixin` logic to focus first non-disabled item.
128-
super.focus();
133+
super.focus(options);
129134
}
130135
}
131136

@@ -281,13 +286,13 @@ export const ListMixin = (superClass) =>
281286
* @param {number} idx
282287
* @protected
283288
*/
284-
_focus(idx) {
289+
_focus(idx, options) {
285290
this.items.forEach((e, index) => {
286291
e.focused = index === idx;
287292
});
288293
this._setFocusable(idx);
289294
this._scrollToItem(idx);
290-
super._focus(idx);
295+
super._focus(idx, options);
291296
}
292297

293298
/**

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
@@ -81,6 +81,16 @@ describe('DelegateFocusMixin', () => {
8181
expect(element.hasAttribute('focused')).to.be.false;
8282
});
8383

84+
it('should set focus-ring attribute when calling focus() by default', () => {
85+
element.focus();
86+
expect(element.hasAttribute('focus-ring')).to.be.true;
87+
});
88+
89+
it('should not set focus-ring attribute when calling focus() with focusVisible: false', () => {
90+
element.focus({ focusVisible: false });
91+
expect(element.hasAttribute('focus-ring')).to.be.false;
92+
});
93+
8494
it('should propagate disabled property to the input', () => {
8595
element.disabled = true;
8696
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
@@ -55,4 +55,14 @@ describe('FocusMixin', () => {
5555
focusin(input);
5656
expect(element.hasAttribute('focus-ring')).to.be.false;
5757
});
58+
59+
it('should set focus-ring attribute when calling focus() by default', () => {
60+
element.focus();
61+
expect(element.hasAttribute('focus-ring')).to.be.true;
62+
});
63+
64+
it('should not set focus-ring attribute when calling focus() with focusVisible: false', () => {
65+
element.focus({ focusVisible: false });
66+
expect(element.hasAttribute('focus-ring')).to.be.false;
67+
});
5868
});

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
@@ -250,6 +250,15 @@ describe('ListMixin', () => {
250250
expect(spy.calledOnce).to.be.true;
251251
});
252252

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

0 commit comments

Comments
 (0)