@@ -21,12 +21,11 @@ function ariaDropdownFn(...args) {
21
21
// it means that this call will reset the dropdown internal settings, then we need to re-delegate the callbacks.
22
22
const needDelegate = ( ! args . length || typeof args [ 0 ] !== 'string' ) ;
23
23
for ( const el of this ) {
24
- const $dropdown = $ ( el ) ;
25
24
if ( ! el [ ariaPatchKey ] ) {
26
- attachInit ( $dropdown ) ;
25
+ attachInit ( el ) ;
27
26
}
28
27
if ( needDelegate ) {
29
- delegateOne ( $dropdown ) ;
28
+ delegateOne ( $ ( el ) ) ;
30
29
}
31
30
}
32
31
return ret ;
@@ -40,17 +39,23 @@ function updateMenuItem(dropdown, item) {
40
39
item . setAttribute ( 'tabindex' , '-1' ) ;
41
40
for ( const el of item . querySelectorAll ( 'a, input, button' ) ) el . setAttribute ( 'tabindex' , '-1' ) ;
42
41
}
43
-
44
- // make the label item and its "delete icon" has correct aria attributes
45
- function updateSelectionLabel ( $label ) {
42
+ /**
43
+ * make the label item and its "delete icon" have correct aria attributes
44
+ * @param {HTMLElement } label
45
+ */
46
+ function updateSelectionLabel ( label ) {
46
47
// the "label" is like this: "<a|div class="ui label" data-value="1">the-label-name <i|svg class="delete icon"/></a>"
47
- if ( ! $label . attr ( 'id' ) ) $label . attr ( 'id' , generateAriaId ( ) ) ;
48
- $label . attr ( 'tabindex' , '-1' ) ;
49
- $label . find ( '.delete.icon' ) . attr ( {
50
- 'aria-hidden' : 'false' ,
51
- 'aria-label' : window . config . i18n . remove_label_str . replace ( '%s' , $label . attr ( 'data-value' ) ) ,
52
- 'role' : 'button' ,
53
- } ) ;
48
+ if ( ! label . id ) {
49
+ label . id = generateAriaId ( ) ;
50
+ }
51
+ label . tabIndex = - 1 ;
52
+
53
+ const deleteIcon = label . querySelector ( '.delete.icon' ) ;
54
+ if ( deleteIcon ) {
55
+ deleteIcon . setAttribute ( 'aria-hidden' , 'false' ) ;
56
+ deleteIcon . setAttribute ( 'aria-label' , window . config . i18n . remove_label_str . replace ( '%s' , label . getAttribute ( 'data-value' ) ) ) ;
57
+ deleteIcon . setAttribute ( 'role' , 'button' ) ;
58
+ }
54
59
}
55
60
56
61
// delegate the dropdown's template functions and callback functions to add aria attributes.
@@ -86,43 +91,44 @@ function delegateOne($dropdown) {
86
91
const dropdownOnLabelCreateOld = dropdownCall ( 'setting' , 'onLabelCreate' ) ;
87
92
dropdownCall ( 'setting' , 'onLabelCreate' , function ( value , text ) {
88
93
const $label = dropdownOnLabelCreateOld . call ( this , value , text ) ;
89
- updateSelectionLabel ( $label ) ;
94
+ updateSelectionLabel ( $label [ 0 ] ) ;
90
95
return $label ;
91
96
} ) ;
92
97
}
93
98
94
99
// for static dropdown elements (generated by server-side template), prepare them with necessary aria attributes
95
- function attachStaticElements ( $dropdown , $focusable , $menu ) {
96
- const dropdown = $dropdown [ 0 ] ;
97
-
100
+ function attachStaticElements ( dropdown , focusable , menu ) {
98
101
// prepare static dropdown menu list popup
99
- if ( ! $menu . attr ( 'id' ) ) $menu . attr ( 'id' , generateAriaId ( ) ) ;
100
- $menu . find ( '> .item' ) . each ( ( _ , item ) => updateMenuItem ( dropdown , item ) ) ;
102
+ if ( ! menu . id ) {
103
+ menu . id = generateAriaId ( ) ;
104
+ }
105
+
106
+ $ ( menu ) . find ( '> .item' ) . each ( ( _ , item ) => updateMenuItem ( dropdown , item ) ) ;
107
+
101
108
// this role could only be changed after its content is ready, otherwise some browsers+readers (like Chrome+AppleVoice) crash
102
- $ menu. attr ( 'role' , dropdown [ ariaPatchKey ] . listPopupRole ) ;
109
+ menu . setAttribute ( 'role' , dropdown [ ariaPatchKey ] . listPopupRole ) ;
103
110
104
111
// prepare selection label items
105
- $dropdown . find ( '.ui.label' ) . each ( ( _ , label ) => updateSelectionLabel ( $ ( label ) ) ) ;
112
+ for ( const label of dropdown . querySelectorAll ( '.ui.label' ) ) {
113
+ updateSelectionLabel ( label ) ;
114
+ }
106
115
107
116
// make the primary element (focusable) aria-friendly
108
- $focusable . attr ( {
109
- 'role' : $focusable . attr ( 'role' ) ?? dropdown [ ariaPatchKey ] . focusableRole ,
110
- 'aria-haspopup' : dropdown [ ariaPatchKey ] . listPopupRole ,
111
- 'aria-controls' : $menu . attr ( 'id' ) ,
112
- 'aria-expanded' : 'false' ,
113
- } ) ;
117
+ focusable . setAttribute ( 'role' , focusable . getAttribute ( 'role' ) ?? dropdown [ ariaPatchKey ] . focusableRole ) ;
118
+ focusable . setAttribute ( 'aria-haspopup' , dropdown [ ariaPatchKey ] . listPopupRole ) ;
119
+ focusable . setAttribute ( 'aria-controls' , menu . id ) ;
120
+ focusable . setAttribute ( 'aria-expanded' , 'false' ) ;
114
121
115
122
// use tooltip's content as aria-label if there is no aria-label
116
- const tooltipContent = $ dropdown. attr ( 'data-tooltip-content' ) ;
117
- if ( tooltipContent && ! $ dropdown. attr ( 'aria-label' ) ) {
118
- $ dropdown. attr ( 'aria-label' , tooltipContent ) ;
123
+ const tooltipContent = dropdown . getAttribute ( 'data-tooltip-content' ) ;
124
+ if ( tooltipContent && ! dropdown . getAttribute ( 'aria-label' ) ) {
125
+ dropdown . setAttribute ( 'aria-label' , tooltipContent ) ;
119
126
}
120
127
}
121
128
122
- function attachInit ( $dropdown ) {
123
- const dropdown = $dropdown [ 0 ] ;
129
+ function attachInit ( dropdown ) {
124
130
dropdown [ ariaPatchKey ] = { } ;
125
- if ( $ dropdown. hasClass ( 'custom' ) ) return ;
131
+ if ( dropdown . classList . contains ( 'custom' ) ) return ;
126
132
127
133
// Dropdown has 2 different focusing behaviors
128
134
// * with search input: the input is focused, and it works with aria-activedescendant pointing another sibling element.
@@ -139,64 +145,66 @@ function attachInit($dropdown) {
139
145
140
146
// TODO: multiple selection is only partially supported. Check and test them one by one in the future.
141
147
142
- const $ textSearch = $ dropdown. find ( 'input.search' ) . eq ( 0 ) ;
143
- const $ focusable = $ textSearch. length ? $textSearch : $ dropdown; // the primary element for focus, see comment above
144
- if ( ! $ focusable. length ) return ;
148
+ const textSearch = dropdown . querySelector ( 'input.search' ) ;
149
+ const focusable = textSearch || dropdown ; // the primary element for focus, see comment above
150
+ if ( ! focusable ) return ;
145
151
146
152
// as a combobox, the input should not have autocomplete by default
147
- if ( $ textSearch. length && ! $ textSearch. attr ( 'autocomplete' ) ) {
148
- $ textSearch. attr ( 'autocomplete' , 'off' ) ;
153
+ if ( textSearch && ! textSearch . getAttribute ( 'autocomplete' ) ) {
154
+ textSearch . setAttribute ( 'autocomplete' , 'off' ) ;
149
155
}
150
156
151
- let $ menu = $dropdown . find ( '> .menu' ) ;
152
- if ( ! $ menu. length ) {
157
+ let menu = $ ( dropdown ) . find ( '> .menu' ) [ 0 ] ;
158
+ if ( ! menu ) {
153
159
// some "multiple selection" dropdowns don't have a static menu element in HTML, we need to pre-create it to make it have correct aria attributes
154
- $menu = $ ( '<div class="menu"></div>' ) . appendTo ( $dropdown ) ;
160
+ menu = document . createElement ( 'div' ) ;
161
+ menu . classList . add ( 'menu' ) ;
162
+ dropdown . append ( menu ) ;
155
163
}
156
164
157
165
// There are 2 possible solutions about the role: combobox or menu.
158
166
// The idea is that if there is an input, then it's a combobox, otherwise it's a menu.
159
167
// Since #19861 we have prepared the "combobox" solution, but didn't get enough time to put it into practice and test before.
160
- const isComboBox = $ dropdown. find ( 'input' ) . length > 0 ;
168
+ const isComboBox = dropdown . querySelectorAll ( 'input' ) . length > 0 ;
161
169
162
170
dropdown [ ariaPatchKey ] . focusableRole = isComboBox ? 'combobox' : 'menu' ;
163
171
dropdown [ ariaPatchKey ] . listPopupRole = isComboBox ? 'listbox' : '' ;
164
172
dropdown [ ariaPatchKey ] . listItemRole = isComboBox ? 'option' : 'menuitem' ;
165
173
166
- attachDomEvents ( $ dropdown, $ focusable, $ menu) ;
167
- attachStaticElements ( $ dropdown, $ focusable, $ menu) ;
174
+ attachDomEvents ( dropdown , focusable , menu ) ;
175
+ attachStaticElements ( dropdown , focusable , menu ) ;
168
176
}
169
177
170
- function attachDomEvents ( $dropdown , $focusable , $menu ) {
171
- const dropdown = $dropdown [ 0 ] ;
178
+ function attachDomEvents ( dropdown , focusable , menu ) {
172
179
// when showing, it has class: ".animating.in"
173
180
// when hiding, it has class: ".visible.animating.out"
174
- const isMenuVisible = ( ) => ( $ menu. hasClass ( 'visible' ) && ! $ menu. hasClass ( 'out' ) ) || $ menu. hasClass ( 'in' ) ;
181
+ const isMenuVisible = ( ) => ( menu . classList . contains ( 'visible' ) && ! menu . classList . contains ( 'out' ) ) || menu . classList . contains ( 'in' ) ;
175
182
176
183
// update aria attributes according to current active/selected item
177
184
const refreshAriaActiveItem = ( ) => {
178
185
const menuVisible = isMenuVisible ( ) ;
179
- $ focusable. attr ( 'aria-expanded' , menuVisible ? 'true' : 'false' ) ;
186
+ focusable . setAttribute ( 'aria-expanded' , menuVisible ? 'true' : 'false' ) ;
180
187
181
188
// if there is an active item, use it (the user is navigating between items)
182
189
// otherwise use the "selected" for combobox (for the last selected item)
183
- const $active = $menu . find ( '> .item.active, > .item.selected' ) ;
190
+ const active = $ ( menu ) . find ( '> .item.active, > .item.selected' ) [ 0 ] ;
191
+ if ( ! active ) return ;
184
192
// if the popup is visible and has an active/selected item, use its id as aria-activedescendant
185
193
if ( menuVisible ) {
186
- $ focusable. attr ( 'aria-activedescendant' , $ active. attr ( 'id' ) ) ;
194
+ focusable . setAttribute ( 'aria-activedescendant' , active . id ) ;
187
195
} else if ( dropdown [ ariaPatchKey ] . listPopupRole === 'menu' ) {
188
196
// for menu, when the popup is hidden, no need to keep the aria-activedescendant, and clear the active/selected item
189
- $ focusable. removeAttr ( 'aria-activedescendant' ) ;
190
- $ active. removeClass ( 'active' ) . removeClass ( 'selected' ) ;
197
+ focusable . removeAttribute ( 'aria-activedescendant' ) ;
198
+ active . classList . remove ( 'active' , 'selected' ) ;
191
199
}
192
200
} ;
193
201
194
- $ dropdown. on ( 'keydown' , ( e ) => {
202
+ dropdown . addEventListener ( 'keydown' , ( e ) => {
195
203
// here it must use keydown event before dropdown's keyup handler, otherwise there is no Enter event in our keyup handler
196
204
if ( e . key === 'Enter' ) {
197
- const dropdownCall = fomanticDropdownFn . bind ( $dropdown ) ;
205
+ const dropdownCall = fomanticDropdownFn . bind ( $ ( dropdown ) ) ;
198
206
let $item = dropdownCall ( 'get item' , dropdownCall ( 'get value' ) ) ;
199
- if ( ! $item ) $item = $menu . find ( '> .item.selected' ) ; // when dropdown filters items by input, there is no "value", so query the "selected" item
207
+ if ( ! $item ) $item = $ ( menu ) . find ( '> .item.selected' ) ; // when dropdown filters items by input, there is no "value", so query the "selected" item
200
208
// if the selected item is clickable, then trigger the click event.
201
209
// we can not click any item without check, because Fomantic code might also handle the Enter event. that would result in double click.
202
210
if ( $item && ( $item [ 0 ] . matches ( 'a' ) || $item . hasClass ( 'js-aria-clickable' ) ) ) $item [ 0 ] . click ( ) ;
@@ -209,7 +217,7 @@ function attachDomEvents($dropdown, $focusable, $menu) {
209
217
// without the delay for hiding, the UI will be somewhat laggy and sometimes may get stuck in the animation.
210
218
const deferredRefreshAriaActiveItem = ( delay = 0 ) => { setTimeout ( refreshAriaActiveItem , delay ) } ;
211
219
dropdown [ ariaPatchKey ] . deferredRefreshAriaActiveItem = deferredRefreshAriaActiveItem ;
212
- $ dropdown. on ( 'keyup' , ( e ) => { if ( e . key . startsWith ( 'Arrow' ) ) deferredRefreshAriaActiveItem ( ) ; } ) ;
220
+ dropdown . addEventListener ( 'keyup' , ( e ) => { if ( e . key . startsWith ( 'Arrow' ) ) deferredRefreshAriaActiveItem ( ) ; } ) ;
213
221
214
222
// if the dropdown has been opened by focus, do not trigger the next click event again.
215
223
// otherwise the dropdown will be closed immediately, especially on Android with TalkBack
0 commit comments