@@ -21,12 +21,11 @@ function ariaDropdownFn(...args) {
2121 // it means that this call will reset the dropdown internal settings, then we need to re-delegate the callbacks.
2222 const needDelegate = ( ! args . length || typeof args [ 0 ] !== 'string' ) ;
2323 for ( const el of this ) {
24- const $dropdown = $ ( el ) ;
2524 if ( ! el [ ariaPatchKey ] ) {
26- attachInit ( $dropdown ) ;
25+ attachInit ( el ) ;
2726 }
2827 if ( needDelegate ) {
29- delegateOne ( $dropdown ) ;
28+ delegateOne ( $ ( el ) ) ;
3029 }
3130 }
3231 return ret ;
@@ -40,17 +39,23 @@ function updateMenuItem(dropdown, item) {
4039 item . setAttribute ( 'tabindex' , '-1' ) ;
4140 for ( const el of item . querySelectorAll ( 'a, input, button' ) ) el . setAttribute ( 'tabindex' , '-1' ) ;
4241}
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 ) {
4647 // 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+ }
5459}
5560
5661// delegate the dropdown's template functions and callback functions to add aria attributes.
@@ -86,43 +91,44 @@ function delegateOne($dropdown) {
8691 const dropdownOnLabelCreateOld = dropdownCall ( 'setting' , 'onLabelCreate' ) ;
8792 dropdownCall ( 'setting' , 'onLabelCreate' , function ( value , text ) {
8893 const $label = dropdownOnLabelCreateOld . call ( this , value , text ) ;
89- updateSelectionLabel ( $label ) ;
94+ updateSelectionLabel ( $label [ 0 ] ) ;
9095 return $label ;
9196 } ) ;
9297}
9398
9499// 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 ) {
98101 // 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+
101108 // 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 ) ;
103110
104111 // 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+ }
106115
107116 // 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' ) ;
114121
115122 // 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 ) ;
119126 }
120127}
121128
122- function attachInit ( $dropdown ) {
123- const dropdown = $dropdown [ 0 ] ;
129+ function attachInit ( dropdown ) {
124130 dropdown [ ariaPatchKey ] = { } ;
125- if ( $ dropdown. hasClass ( 'custom' ) ) return ;
131+ if ( dropdown . classList . contains ( 'custom' ) ) return ;
126132
127133 // Dropdown has 2 different focusing behaviors
128134 // * with search input: the input is focused, and it works with aria-activedescendant pointing another sibling element.
@@ -139,64 +145,66 @@ function attachInit($dropdown) {
139145
140146 // TODO: multiple selection is only partially supported. Check and test them one by one in the future.
141147
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 ;
145151
146152 // 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' ) ;
149155 }
150156
151- let $ menu = $dropdown . find ( '> .menu' ) ;
152- if ( ! $ menu. length ) {
157+ let menu = $ ( dropdown ) . find ( '> .menu' ) [ 0 ] ;
158+ if ( ! menu ) {
153159 // 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 ) ;
155163 }
156164
157165 // There are 2 possible solutions about the role: combobox or menu.
158166 // The idea is that if there is an input, then it's a combobox, otherwise it's a menu.
159167 // 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 ;
161169
162170 dropdown [ ariaPatchKey ] . focusableRole = isComboBox ? 'combobox' : 'menu' ;
163171 dropdown [ ariaPatchKey ] . listPopupRole = isComboBox ? 'listbox' : '' ;
164172 dropdown [ ariaPatchKey ] . listItemRole = isComboBox ? 'option' : 'menuitem' ;
165173
166- attachDomEvents ( $ dropdown, $ focusable, $ menu) ;
167- attachStaticElements ( $ dropdown, $ focusable, $ menu) ;
174+ attachDomEvents ( dropdown , focusable , menu ) ;
175+ attachStaticElements ( dropdown , focusable , menu ) ;
168176}
169177
170- function attachDomEvents ( $dropdown , $focusable , $menu ) {
171- const dropdown = $dropdown [ 0 ] ;
178+ function attachDomEvents ( dropdown , focusable , menu ) {
172179 // when showing, it has class: ".animating.in"
173180 // 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' ) ;
175182
176183 // update aria attributes according to current active/selected item
177184 const refreshAriaActiveItem = ( ) => {
178185 const menuVisible = isMenuVisible ( ) ;
179- $ focusable. attr ( 'aria-expanded' , menuVisible ? 'true' : 'false' ) ;
186+ focusable . setAttribute ( 'aria-expanded' , menuVisible ? 'true' : 'false' ) ;
180187
181188 // if there is an active item, use it (the user is navigating between items)
182189 // 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 ;
184192 // if the popup is visible and has an active/selected item, use its id as aria-activedescendant
185193 if ( menuVisible ) {
186- $ focusable. attr ( 'aria-activedescendant' , $ active. attr ( 'id' ) ) ;
194+ focusable . setAttribute ( 'aria-activedescendant' , active . id ) ;
187195 } else if ( dropdown [ ariaPatchKey ] . listPopupRole === 'menu' ) {
188196 // 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' ) ;
191199 }
192200 } ;
193201
194- $ dropdown. on ( 'keydown' , ( e ) => {
202+ dropdown . addEventListener ( 'keydown' , ( e ) => {
195203 // here it must use keydown event before dropdown's keyup handler, otherwise there is no Enter event in our keyup handler
196204 if ( e . key === 'Enter' ) {
197- const dropdownCall = fomanticDropdownFn . bind ( $dropdown ) ;
205+ const dropdownCall = fomanticDropdownFn . bind ( $ ( dropdown ) ) ;
198206 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
200208 // if the selected item is clickable, then trigger the click event.
201209 // we can not click any item without check, because Fomantic code might also handle the Enter event. that would result in double click.
202210 if ( $item && ( $item [ 0 ] . matches ( 'a' ) || $item . hasClass ( 'js-aria-clickable' ) ) ) $item [ 0 ] . click ( ) ;
@@ -209,7 +217,7 @@ function attachDomEvents($dropdown, $focusable, $menu) {
209217 // without the delay for hiding, the UI will be somewhat laggy and sometimes may get stuck in the animation.
210218 const deferredRefreshAriaActiveItem = ( delay = 0 ) => { setTimeout ( refreshAriaActiveItem , delay ) } ;
211219 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 ( ) ; } ) ;
213221
214222 // if the dropdown has been opened by focus, do not trigger the next click event again.
215223 // otherwise the dropdown will be closed immediately, especially on Android with TalkBack
0 commit comments