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