import { ApproxStructure, Assertions, Keyboard, Keys, type TestStore, UiFinder } from '@ephox/agar'; import { context, describe, it } from '@ephox/bedrock-client'; import { Arr, Optional } from '@ephox/katamari'; import type { SugarElement } from '@ephox/sugar'; import * as AddEventsBehaviour from 'ephox/alloy/api/behaviour/AddEventsBehaviour'; import * as Behaviour from 'ephox/alloy/api/behaviour/Behaviour'; import type { AlloyComponent } from 'ephox/alloy/api/component/ComponentApi'; import * as GuiFactory from 'ephox/alloy/api/component/GuiFactory'; import type { AlloySpec } from 'ephox/alloy/api/component/SpecTypes'; import * as AlloyEvents from 'ephox/alloy/api/events/AlloyEvents'; import * as AlloyTriggers from 'ephox/alloy/api/events/AlloyTriggers'; import * as SystemEvents from 'ephox/alloy/api/events/SystemEvents'; import { Menu } from 'ephox/alloy/api/ui/Menu'; import * as MenuEvents from 'ephox/alloy/menu/util/MenuEvents'; import * as TestDropdownMenu from 'ephox/alloy/test/dropdown/TestDropdownMenu'; import * as GuiSetup from 'ephox/alloy/test/GuiSetup'; import type { MenuMovementSpec } from 'ephox/alloy/ui/types/MenuTypes'; describe('browser.ui.dropdown.MatrixMenuTest', () => { const selectedClass = 'selected-item'; const assertSelectedStates = (label: string, expectedPos: number, menu: SugarElement) => Assertions.assertStructure( label, ApproxStructure.build( (s, _str, arr) => { const classcheck = (index: number) => index === expectedPos ? arr.has(selectedClass) : arr.not(selectedClass); return s.element('ol', { classes: [ arr.has('test-menu') ], children: [ s.element('div', { classes: [ arr.has('row-class') ], children: [ s.element('li', { classes: [ classcheck(0) ] }), s.element('li', { classes: [ classcheck(1) ] }) ] }), s.element('div', { classes: [ arr.has('row-class') ], children: [ s.element('li', { classes: [ classcheck(2) ] }), s.element('li', { classes: [ classcheck(3) ] }) ] }) ] }); } ), menu); const makeGuiHook = (movementSpec: MenuMovementSpec) => { return GuiSetup.bddSetup( (store, _doc, _body) => GuiFactory.build( Menu.sketch({ value: 'test-menu-1', items: Arr.map([ { type: 'item', data: { value: 'alpha', meta: { }}, hasSubmenu: false }, { type: 'item', data: { value: 'beta', meta: { }}, hasSubmenu: false }, { type: 'item', data: { value: 'gamma', meta: { }}, hasSubmenu: false }, { type: 'item', data: { value: 'delta', meta: { }}, hasSubmenu: false } ], TestDropdownMenu.renderItem), dom: { tag: 'ol', classes: [ 'test-menu' ] }, movement: movementSpec, components: [ Menu.parts.items({ preprocess: (items: AlloySpec[]) => { const chunks = Arr.chunk(items, 2); return Arr.map(chunks, (c) => ({ dom: { tag: 'div', classes: [ 'row-class' ] }, components: c })); } }) ], markers: { item: TestDropdownMenu.markers().item, selectedItem: TestDropdownMenu.markers().selectedItem }, menuBehaviours: Behaviour.derive([ AddEventsBehaviour.config('menu-test-behaviour', [ AlloyEvents.run(MenuEvents.focus(), store.adder('menu.events.focus')) ]) ]) }) ) ); }; const assertInitialFocusIsOnElement = (store: TestStore, menuComponent: AlloyComponent, position: number, message: string) => { AlloyTriggers.dispatch(menuComponent, menuComponent.element, SystemEvents.focus()); assertFocusIsOnItem(menuComponent, position, message); store.assertEq('After focusItem event', [ 'menu.events.focus' ]); store.clear(); }; const assertFocusIsOnItem = (menuComponent: AlloyComponent, position: number, message: string) => assertSelectedStates(message, position, menuComponent.element); const setFocusOnItem = (store: TestStore, menuComponent: AlloyComponent, element: SugarElement, position: number, message: string) => { AlloyTriggers.dispatch(menuComponent, element, SystemEvents.focusItem()); // Focus item on alpha should select alpha (the first item) assertFocusIsOnItem(menuComponent, position, message); store.assertEq('After focusItem event', [ 'menu.events.focus' ]); store.clear(); }; const keyboardNavigationMovement = (store: TestStore, menuComponent: AlloyComponent, key: number, position: number, message: string) => { Keyboard.keydown(key, { }, menuComponent.element); assertFocusIsOnItem(menuComponent, position, message); store.assertEq('After pressing key', [ 'menu.events.focus' ]); store.clear(); }; context('No previous selector', () => { const hook = makeGuiHook({ mode: 'matrix', rowSelector: '.row-class' }); it('TINY-9283: Basic Focusing', () => { const menuComponent = hook.component(); const store = hook.store(); // Find the alpha and beta list items const alphaItem = UiFinder.findIn(menuComponent.element, 'li[data-value="alpha"]').getOrDie(); const betaItem = UiFinder.findIn(menuComponent.element, 'li[data-value="beta"]').getOrDie(); store.assertEq('Before focusItem event', [ ]); assertInitialFocusIsOnElement(store, menuComponent, 0, 'Focus should be on alpha'); setFocusOnItem(store, menuComponent, alphaItem, 0, 'After focusing on alpha'); setFocusOnItem(store, menuComponent, betaItem, 1, 'After focusing on beta'); }); it('TINY-9283: Keyboard navigation', () => { const menuComponent = hook.component(); const store = hook.store(); store.assertEq('Before focusItem event', [ ]); assertInitialFocusIsOnElement(store, menuComponent, 0, 'Focus should be on alpha'); keyboardNavigationMovement(store, menuComponent, Keys.right(), 1, 'Press right to go from alpha to beta ( column movement )'); keyboardNavigationMovement(store, menuComponent, Keys.down(), 3, 'Press down to move from beta to delta ( row movement )'); keyboardNavigationMovement(store, menuComponent, Keys.left(), 2, 'Press left to go from delta to gamma ( column movement )'); keyboardNavigationMovement(store, menuComponent, Keys.up(), 0, 'Press up to move from gamma to alpha ( row movement )'); }); }); context('Previous selector', () => { const hook = makeGuiHook({ mode: 'matrix', rowSelector: '.row-class', previousSelector: (component) => Optional.some(UiFinder.findIn(component.element, 'li[data-value="beta"]').getOrDie()) }); it('TINY-9283: Position starts as expected', () => { const menuComponent = hook.component(); const store = hook.store(); assertInitialFocusIsOnElement(store, menuComponent, 1, 'Focus should be on beta'); }); }); });