| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245 |
1×
45574×
45574×
45574×
45574×
45574×
45574×
1×
1×
47001×
47001×
47001×
47001×
47001×
1×
92575×
92575×
92575×
92575×
92575×
1×
139961×
48813×
47386×
1427×
139961×
1×
385×
1×
1×
7881×
7881×
3441×
4440×
4440×
530×
530×
4440×
1×
3441×
3441×
3441×
1×
7881×
7881×
7881×
4440×
3441×
3441×
1×
8×
8×
8×
1×
1×
| /**
* The TabIndexedElement class is an attribute mixin used to add additional functionality to an
* element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
* order in which users will navigate through the focusable elements via the Tab key.
*
* @example
* // TabIndexedElement is mixed into the ButtonWidget class
* // to provide a tabIndex property.
* const button1 = new OO.ui.ButtonWidget( {
* label: 'fourth',
* tabIndex: 4
* } ),
* button2 = new OO.ui.ButtonWidget( {
* label: 'second',
* tabIndex: 2
* } ),
* button3 = new OO.ui.ButtonWidget( {
* label: 'third',
* tabIndex: 3
* } ),
* button4 = new OO.ui.ButtonWidget( {
* label: 'first',
* tabIndex: 1
* } );
* $( document.body ).append(
* button1.$element,
* button2.$element,
* button3.$element,
* button4.$element
* );
*
* @abstract
* @class
*
* @constructor
* @param {Object} [config] Configuration options
* @param {jQuery} [config.$tabIndexed] The element that should use the tabindex functionality. By default,
* the functionality is applied to the element created by the class ($element). If a different
* element is specified, the tabindex functionality will be applied to it instead.
* @param {string|number|null} [config.tabIndex=0] Number that specifies the element’s position in the
* tab-navigation order (e.g., 1 for the first focusable element). Use 0 to use the default
* navigation order; use -1 to remove the element from the tab-navigation flow.
*/
OO.ui.mixin.TabIndexedElement = function OoUiMixinTabIndexedElement( config ) {
// Configuration initialization
config = Object.assign( { tabIndex: 0 }, config );
// Properties
this.$tabIndexed = null;
this.tabIndex = null;
// Events
this.connect( this, {
disable: 'onTabIndexedElementDisable'
} );
// Initialization
this.setTabIndex( config.tabIndex );
this.setTabIndexedElement( config.$tabIndexed || this.$element );
};
/* Setup */
OO.initClass( OO.ui.mixin.TabIndexedElement );
/* Methods */
/**
* Set the element that should use the tabindex functionality.
*
* This method is used to retarget a tabindex mixin so that its functionality applies
* to the specified element. If an element is currently using the functionality, the mixin’s
* effect on that element is removed before the new element is set up.
*
* @param {jQuery} $tabIndexed Element that should use the tabindex functionality
* @chainable
* @return {OO.ui.Element} The element, for chaining
*/
OO.ui.mixin.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
const tabIndex = this.tabIndex;
// Remove attributes from old $tabIndexed
this.setTabIndex( null );
// Force update of new $tabIndexed
this.$tabIndexed = $tabIndexed;
this.tabIndex = tabIndex;
return this.updateTabIndex();
};
/**
* Set the value of the tabindex.
*
* @param {string|number|null} tabIndex Tabindex value, or `null` for no tabindex
* @chainable
* @return {OO.ui.Element} The element, for chaining
*/
OO.ui.mixin.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
tabIndex = /^-?\d+$/.test( tabIndex ) ? Number( tabIndex ) : null;
Eif ( this.tabIndex !== tabIndex ) {
this.tabIndex = tabIndex;
this.updateTabIndex();
}
return this;
};
/**
* Update the `tabindex` attribute, in case of changes to tab index or
* disabled state.
*
* @private
* @chainable
* @return {OO.ui.Element} The element, for chaining
*/
OO.ui.mixin.TabIndexedElement.prototype.updateTabIndex = function () {
if ( this.$tabIndexed ) {
if ( this.tabIndex !== null ) {
// Do not index over disabled elements
this.$tabIndexed.attr( {
tabindex: this.isDisabled() ? -1 : this.tabIndex,
// Support: ChromeVox and NVDA
// These do not seem to inherit aria-disabled from parent elements
'aria-disabled': this.isDisabled() ? 'true' : null
} );
} else {
this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
}
}
return this;
};
/**
* Handle disable events.
*
* @private
* @param {boolean} disabled Element is disabled
*/
OO.ui.mixin.TabIndexedElement.prototype.onTabIndexedElementDisable = function () {
this.updateTabIndex();
};
/**
* Get the value of the tabindex.
*
* @return {number|null} Tabindex value
*/
OO.ui.mixin.TabIndexedElement.prototype.getTabIndex = function () {
return this.tabIndex;
};
/**
* Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
*
* If the element already has an ID then that is returned, otherwise unique ID is
* generated, set on the element, and returned.
*
* @return {string|null} The ID of the focusable element
*/
OO.ui.mixin.TabIndexedElement.prototype.getInputId = function () {
Iif ( !this.$tabIndexed ) {
return null;
}
if ( !this.isLabelableNode( this.$tabIndexed ) ) {
return null;
}
let id = this.$tabIndexed.attr( 'id' );
if ( id === undefined ) {
id = OO.ui.generateElementId();
this.$tabIndexed.attr( 'id', id );
}
return id;
};
/**
* Set the element with the given ID as a label for this widget.
*
* @param {string|null} id
*/
OO.ui.mixin.TabIndexedElement.prototype.setLabelledBy = function ( id ) {
Iif ( !this.$tabIndexed ) {
return;
}
Eif ( id ) {
this.$tabIndexed.attr( 'aria-labelledby', id );
} else {
this.$tabIndexed.removeAttr( 'aria-labelledby' );
}
};
/**
* Whether the node is 'labelable' according to the HTML spec
* (i.e., whether it can be interacted with through a `<label for="…">`).
* See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
*
* @private
* @param {jQuery} $node
* @return {boolean}
*/
OO.ui.mixin.TabIndexedElement.prototype.isLabelableNode = function ( $node ) {
const
labelableTags = [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
tagName = ( $node.prop( 'tagName' ) || '' ).toLowerCase();
if ( tagName === 'input' && $node.attr( 'type' ) !== 'hidden' ) {
return true;
}
Iif ( labelableTags.includes( tagName ) ) {
return true;
}
return false;
};
/**
* Focus this element.
*
* @chainable
* @return {OO.ui.Element} The element, for chaining
*/
OO.ui.mixin.TabIndexedElement.prototype.focus = function () {
Eif ( !this.isDisabled() ) {
this.$tabIndexed.trigger( 'focus' );
}
return this;
};
/**
* Blur this element.
*
* @chainable
* @return {OO.ui.Element} The element, for chaining
*/
OO.ui.mixin.TabIndexedElement.prototype.blur = function () {
this.$tabIndexed.trigger( 'blur' );
return this;
};
/**
* @inheritdoc OO.ui.Widget
*/
OO.ui.mixin.TabIndexedElement.prototype.simulateLabelClick = function () {
this.focus();
};
|