11import type { ToolbarItemComponent } from '@js/common' ;
2+ import { keyboard } from '@js/common/core/events/short' ;
23import type { DataSourceOptions } from '@js/common/data' ;
34import type { dxElementWrapper } from '@js/core/renderer' ;
45import $ from '@js/core/renderer' ;
56import { each } from '@js/core/utils/iterator' ;
67import type { DxEvent } from '@js/events' ;
78import type { Item } from '@js/ui/toolbar' ;
9+ import { getPublicElement } from '@ts/core/m_element' ;
810import type { ActionConfig } from '@ts/core/widget/component' ;
11+ import type { SupportedKeys } from '@ts/core/widget/widget' ;
912import type { ItemRenderInfo , ItemTemplate } from '@ts/ui/collection/collection_widget.base' ;
1013import { ListBase } from '@ts/ui/list/list.base' ;
14+ import { closeItemWidget , getItemFocusTarget , isItemWidgetOpened , setItemWidgetFocusState } from '@ts/ui/toolbar/toolbar.utils' ;
1115
1216export const TOOLBAR_MENU_ACTION_CLASS = 'dx-toolbar-menu-action' ;
1317const TOOLBAR_HIDDEN_BUTTON_CLASS = 'dx-toolbar-hidden-button' ;
@@ -19,6 +23,12 @@ const SCROLLVIEW_CONTENT_CLASS = 'dx-scrollview-content';
1923
2024type ActionableComponents = Extract < ToolbarItemComponent , 'dxButton' | 'dxButtonGroup' > ;
2125export default class ToolbarMenuList extends ListBase {
26+ _captureKeydownHandler ?: EventListener ;
27+
28+ _onEscapePress ?: ( ) => void ;
29+
30+ _keyboardListenerId ?: string ;
31+
2232 protected _activeStateUnit ( ) : string {
2333 return `.${ TOOLBAR_MENU_ACTION_CLASS } :not(.${ TOOLBAR_HIDDEN_BUTTON_GROUP_CLASS } )` ;
2434 }
@@ -130,6 +140,323 @@ export default class ToolbarMenuList extends ListBase {
130140 } ;
131141 }
132142
143+ _supportedKeys ( ) : SupportedKeys {
144+ const keys = super . _supportedKeys ( ) ;
145+
146+ delete keys . leftArrow ;
147+ delete keys . rightArrow ;
148+ delete keys . upArrow ;
149+ delete keys . downArrow ;
150+ delete keys . home ;
151+ delete keys . end ;
152+
153+ const originalEnter = keys . enter ;
154+ keys . enter = ( e : DxEvent < KeyboardEvent > ) : void => {
155+ const target = e . target as HTMLElement ;
156+
157+ if ( this . _isTextInputTarget ( target ) || this . _isMenuTarget ( target ) ) {
158+ return ;
159+ }
160+
161+ const { focusedElement } = this . option ( ) ;
162+ const $item = $ ( focusedElement ) ;
163+
164+ if ( $item . length ) {
165+ const $textEditor = $item . find ( '.dx-texteditor-input' ) . first ( ) ;
166+ if ( $textEditor . length ) {
167+ e . preventDefault ( ) ;
168+ ( $textEditor . get ( 0 ) as HTMLElement ) . focus ( ) ;
169+ return ;
170+ }
171+
172+ const $menu = $item . find ( '.dx-menu' ) . first ( ) ;
173+ if ( $menu . length ) {
174+ e . preventDefault ( ) ;
175+ const menuInstance = $menu . data ( 'dxMenu' ) ;
176+ if ( menuInstance ) {
177+ // @ts -expect-error ts-error
178+ menuInstance . focus ( ) ;
179+ }
180+ return ;
181+ }
182+ }
183+
184+ originalEnter ?. call ( this , e ) ;
185+ } ;
186+
187+ return keys ;
188+ }
189+
190+ _attachKeyboardEvents ( ) : void {
191+ this . _detachKeyboardEvents ( ) ;
192+
193+ const { focusStateEnabled } = this . option ( ) ;
194+
195+ if ( focusStateEnabled ) {
196+ this . _keyboardListenerId = keyboard . on (
197+ this . _keyboardEventBindingTarget ( ) ,
198+ null ,
199+ ( opts ) => this . _keyboardHandler ( opts ) ,
200+ ) ;
201+
202+ this . _attachCaptureKeyHandler ( ) ;
203+ }
204+ }
205+
206+ _detachKeyboardEvents ( ) : void {
207+ if ( this . _keyboardListenerId ) {
208+ keyboard . off ( this . _keyboardListenerId ) ;
209+ this . _keyboardListenerId = undefined ;
210+ }
211+
212+ this . _detachCaptureKeyHandler ( ) ;
213+ }
214+
215+ _attachCaptureKeyHandler ( ) : void {
216+ this . _detachCaptureKeyHandler ( ) ;
217+
218+ const element = this . $element ( ) . get ( 0 ) as HTMLElement ;
219+
220+ this . _captureKeydownHandler = ( evt : Event ) : void => {
221+ const e = evt as KeyboardEvent ;
222+ const target = e . target as HTMLElement ;
223+
224+ const isTextInput = this . _isTextInputTarget ( target ) ;
225+ const isMenu = this . _isMenuTarget ( target ) ;
226+
227+ if ( ( isTextInput || isMenu ) && e . key !== 'Escape' ) {
228+ return ;
229+ }
230+
231+ if ( e . key === 'Escape' && ( isTextInput || isMenu ) ) {
232+ e . preventDefault ( ) ;
233+ e . stopPropagation ( ) ;
234+
235+ const $item = $ ( target ) . closest ( this . _itemSelector ( ) ) ;
236+ if ( $item . length && closeItemWidget ( $item ) ) {
237+ return ;
238+ }
239+
240+ if ( $item . length ) {
241+ this . _focusItemWidget ( $item ) ;
242+ }
243+
244+ return ;
245+ }
246+
247+ if ( e . key === 'Escape' ) {
248+ e . preventDefault ( ) ;
249+ e . stopPropagation ( ) ;
250+ this . _onEscapePress ?.( ) ;
251+
252+ return ;
253+ }
254+
255+ const keyToLocation : Record < string , string > = {
256+ ArrowDown : 'down' ,
257+ ArrowUp : 'up' ,
258+ Home : 'first' ,
259+ End : 'last' ,
260+ } ;
261+
262+ const location = keyToLocation [ e . key ] ;
263+
264+ if ( ! location ) {
265+ return ;
266+ }
267+
268+ const { focusedElement } = this . option ( ) ;
269+ const $focused = $ ( focusedElement ) ;
270+ if ( $focused . length && isItemWidgetOpened ( $focused ) ) {
271+ return ;
272+ }
273+
274+ e . preventDefault ( ) ;
275+ e . stopPropagation ( ) ;
276+
277+ this . _moveFocus ( location ) ;
278+ } ;
279+
280+ element . addEventListener ( 'keydown' , this . _captureKeydownHandler , true ) ;
281+ }
282+
283+ _detachCaptureKeyHandler ( ) : void {
284+ if ( this . _captureKeydownHandler ) {
285+ const element = this . $element ( ) . get ( 0 ) as HTMLElement ;
286+ element . removeEventListener ( 'keydown' , this . _captureKeydownHandler , true ) ;
287+ this . _captureKeydownHandler = undefined ;
288+ }
289+ }
290+
291+ _isTextInputTarget ( target : HTMLElement ) : boolean {
292+ const tagName = target . tagName . toLowerCase ( ) ;
293+
294+ return ( tagName === 'input' || tagName === 'textarea' )
295+ && $ ( target ) . closest ( '.dx-texteditor' ) . length > 0 ;
296+ }
297+
298+ _isMenuTarget ( target : HTMLElement ) : boolean {
299+ return $ ( target ) . closest ( '.dx-menu' ) . length > 0 ;
300+ }
301+
302+ _getAvailableItems ( $itemElements ?: dxElementWrapper ) : dxElementWrapper {
303+ const $visible = this . _getVisibleItems ( $itemElements ) ;
304+ const elements = Array . from ( $visible . toArray ( ) ) . filter (
305+ ( item ) => ! ! getItemFocusTarget ( $ ( item ) ) ?. length ,
306+ ) ;
307+
308+ return $ ( elements ) as unknown as dxElementWrapper ;
309+ }
310+
311+ _setFocusedItem ( $target : dxElementWrapper ) : void {
312+ super . _setFocusedItem ( $target ) ;
313+ this . _updateRovingTabIndex ( $target ) ;
314+ }
315+
316+ _updateRovingTabIndex ( $activeItem ?: dxElementWrapper ) : void {
317+ const $items = this . _getAvailableItems ( ) ;
318+ let hasActive = false ;
319+
320+ $items . each ( ( _index : number , item : Element ) : boolean => {
321+ const $item = $ ( item ) ;
322+ const $focusTarget = getItemFocusTarget ( $item ) ;
323+
324+ if ( $focusTarget ?. length ) {
325+ const isActive = ! ! $activeItem ?. length && $item . get ( 0 ) === $activeItem . get ( 0 ) ;
326+ $focusTarget . attr ( 'tabIndex' , isActive ? 0 : - 1 ) ;
327+ if ( isActive ) {
328+ hasActive = true ;
329+ }
330+
331+ const $input = $focusTarget . hasClass ( 'dx-texteditor' )
332+ ? $focusTarget . find ( '.dx-texteditor-input' )
333+ : undefined ;
334+
335+ if ( $input ?. length ) {
336+ if ( ! isActive ) {
337+ $input . attr ( 'tabIndex' , - 1 ) ;
338+ }
339+
340+ const hasDropDown = $focusTarget . hasClass ( 'dx-dropdowneditor' ) ;
341+ if ( ! hasDropDown && ! $focusTarget . attr ( 'role' ) ) {
342+ const label = $input . attr ( 'aria-label' )
343+ ?? $input . attr ( 'placeholder' )
344+ ?? '' ;
345+ // @ts -expect-error ts-error
346+ $focusTarget . attr ( {
347+ role : 'textbox' ,
348+ 'aria-readonly' : 'true' ,
349+ 'aria-label' : label ,
350+ } ) ;
351+ }
352+ }
353+
354+ const $menu = $item . find ( '.dx-menu' ) ;
355+ if ( $menu . length ) {
356+ $menu . attr ( 'tabIndex' , - 1 ) ;
357+ $menu . find ( '[tabindex]' ) . attr ( 'tabIndex' , - 1 ) ;
358+ }
359+ }
360+
361+ return true ;
362+ } ) ;
363+
364+ if ( ! hasActive ) {
365+ const $first = $items . first ( ) ;
366+ if ( $first . length ) {
367+ const $firstTarget = getItemFocusTarget ( $first ) ;
368+ $firstTarget ?. attr ( 'tabIndex' , 0 ) ;
369+ }
370+ }
371+ }
372+
373+ _focusInHandler ( e : DxEvent ) : void {
374+ const $target = $ ( e . target as Element ) ;
375+ const $item = $target . closest ( this . _itemSelector ( ) ) ;
376+
377+ if ( $item . length && getItemFocusTarget ( $item ) ?. length ) {
378+ this . option ( 'focusedElement' , getPublicElement ( $item ) ) ;
379+ }
380+ }
381+
382+ _focusItemWidget ( $item : dxElementWrapper ) : void {
383+ const $focusTarget = getItemFocusTarget ( $item ) ;
384+ if ( ! $focusTarget ?. length ) {
385+ return ;
386+ }
387+
388+ ( $focusTarget . get ( 0 ) as HTMLElement ) . focus ( ) ;
389+ setItemWidgetFocusState ( $item , true ) ;
390+ }
391+
392+ _focusOutHandler ( e : DxEvent ) : void {
393+ const { relatedTarget } = e as DxEvent & { relatedTarget : Element } ;
394+ const target = e . target as Element ;
395+
396+ if ( relatedTarget && this . $element ( ) . get ( 0 ) ?. contains ( relatedTarget ) ) {
397+ return ;
398+ }
399+
400+ if ( relatedTarget && $ ( relatedTarget ) . closest ( '.dx-overlay-content' ) . length ) {
401+ return ;
402+ }
403+
404+ if ( target && $ ( target ) . closest ( '.dx-overlay-content' ) . length ) {
405+ return ;
406+ }
407+
408+ const { focusedElement } = this . option ( ) ;
409+ const $focused = $ ( focusedElement ) ;
410+ if ( $focused . length ) {
411+ setItemWidgetFocusState ( $focused , false ) ;
412+ }
413+
414+ super . _focusOutHandler ( e ) ;
415+ }
416+
417+ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
418+ _moveFocus ( location : string ) : boolean | undefined | void {
419+ const { focusedElement : prevFocusedElement } = this . option ( ) ;
420+ const $prev = $ ( prevFocusedElement ) ;
421+ if ( $prev . length ) {
422+ closeItemWidget ( $prev ) ;
423+ setItemWidgetFocusState ( $prev , false ) ;
424+ }
425+
426+ const result = super . _moveFocus ( location ) ;
427+
428+ const { focusedElement } = this . option ( ) ;
429+ const $focused = $ ( focusedElement ) ;
430+ if ( $focused . length ) {
431+ this . _focusItemWidget ( $focused ) ;
432+ }
433+
434+ return result ;
435+ }
436+
437+ focusFirstItem ( ) : void {
438+ const $first = this . _getAvailableItems ( ) . first ( ) ;
439+ if ( $first . length ) {
440+ this . option ( 'focusedElement' , getPublicElement ( $first ) ) ;
441+ this . _focusItemWidget ( $first ) ;
442+ }
443+ }
444+
445+ focusLastItem ( ) : void {
446+ const $last = this . _getAvailableItems ( ) . last ( ) ;
447+ if ( $last . length ) {
448+ this . option ( 'focusedElement' , getPublicElement ( $last ) ) ;
449+ this . _focusItemWidget ( $last ) ;
450+ }
451+ }
452+
453+ _postProcessRenderItems ( ) : void {
454+ super . _postProcessRenderItems ( ) ;
455+
456+ const { focusedElement } = this . option ( ) ;
457+ this . _updateRovingTabIndex ( $ ( focusedElement ) ) ;
458+ }
459+
133460 _itemClickHandler (
134461 e : DxEvent ,
135462 args ?: Record < string , unknown > ,
@@ -141,6 +468,7 @@ export default class ToolbarMenuList extends ListBase {
141468 }
142469
143470 _clean ( ) : void {
471+ this . _detachCaptureKeyHandler ( ) ;
144472 this . _getSections ( ) . empty ( ) ;
145473 super . _clean ( ) ;
146474 }
0 commit comments