import { ApproxStructure, Assertions, Chain, FocusTools, GeneralSteps, Keyboard, Keys, Logger, Mouse, Step, UiFinder, Waiter } from '@ephox/agar'; import { UnitTest } from '@ephox/bedrock-client'; import { Arr, Fun, Future, Optional, Result } from '@ephox/katamari'; import { Compare, Focus } from '@ephox/sugar'; import * as AddEventsBehaviour from 'ephox/alloy/api/behaviour/AddEventsBehaviour'; import * as Behaviour from 'ephox/alloy/api/behaviour/Behaviour'; import { Positioning } from 'ephox/alloy/api/behaviour/Positioning'; import { Representing } from 'ephox/alloy/api/behaviour/Representing'; import * as GuiFactory from 'ephox/alloy/api/component/GuiFactory'; import * as Memento from 'ephox/alloy/api/component/Memento'; import * as AlloyEvents from 'ephox/alloy/api/events/AlloyEvents'; import * as AlloyTriggers from 'ephox/alloy/api/events/AlloyTriggers'; import { Container } from 'ephox/alloy/api/ui/Container'; import { Dropdown } from 'ephox/alloy/api/ui/Dropdown'; import { tieredMenu as TieredMenu } from 'ephox/alloy/api/ui/TieredMenu'; import * as DropdownAssertions from 'ephox/alloy/test/dropdown/DropdownAssertions'; import type { TestItem } from 'ephox/alloy/test/dropdown/TestDropdownMenu'; import * as TestDropdownMenu from 'ephox/alloy/test/dropdown/TestDropdownMenu'; import * as GuiSetup from 'ephox/alloy/test/GuiSetup'; import * as NavigationUtils from 'ephox/alloy/test/NavigationUtils'; import * as TestBroadcasts from 'ephox/alloy/test/TestBroadcasts'; UnitTest.asynctest('Dropdown List', (success, failure) => { const memSink = Memento.record( Container.sketch({ containerBehaviours: Behaviour.derive([ Positioning.config({ useFixed: Fun.always }) ]) }) ); GuiSetup.setup((store, _doc, _body) => { const c = GuiFactory.build( Dropdown.sketch({ dom: { tag: 'button', classes: [ 'test-dropdown' ] }, components: [ { dom: { tag: 'span', innerHtml: 'hi' } } ], sandboxClasses: [ 'my-test-sandbox' ], sandboxBehaviours: Behaviour.derive([ AddEventsBehaviour.config('test-events', [ AlloyEvents.run('made-up-event', store.adder('received made-up-event')) ]) ]), lazySink: (c) => { TestDropdownMenu.assertLazySinkArgs('button', 'test-dropdown', c); return Result.value(memSink.get(c)); }, toggleClass: 'alloy-selected', dropdownBehaviours: Behaviour.derive( Arr.map([ '1', '2', '3' ], (num) => AddEventsBehaviour.config(`test-listener-${num}`, [ AlloyEvents.run('test-listener', store.adder(`test.listener.${num}`)) ])) ), eventOrder: { 'test-listener': [ 'test-listener-1', 'test-listener-3', 'test-listener-2' ] }, matchWidth: true, parts: { menu: TestDropdownMenu.part(store) }, fetch: () => { const future = Future.pure([ { type: 'item', data: { value: 'alpha', meta: { text: 'Alpha' }}}, { type: 'item', data: { value: 'beta', meta: { text: 'Beta' }}}, { type: 'item', data: { value: 'gamma', meta: { text: 'Gamma' }}}, { type: 'item', data: { value: 'delta', meta: { text: 'Delta' }}} ]); return future.map((f) => { const menu = TestDropdownMenu.renderMenu({ value: 'v', items: Arr.map(f, TestDropdownMenu.renderItem) }); return Optional.some(TieredMenu.singleData('test', menu)); }); } }) ); return c; }, (doc, _body, gui, component, store) => { const sink = GuiFactory.build(memSink.asSpec()); const inputForFocus = GuiFactory.build({ dom: { tag: 'input' } }); gui.add(sink); gui.add(inputForFocus); const focusables = { button: { label: 'dropdown-button', selector: 'button' }, alpha: { label: 'alpha-item', selector: 'li:contains(Alpha)' }, beta: { label: 'beta-item', selector: 'li:contains(Beta)' }, gamma: { label: 'gamma-item', selector: 'li:contains(Gamma)' }, delta: { label: 'delta-item', selector: 'li:contains(Delta)' } }; return [ GuiSetup.mAddStyles(doc, [ ':focus { outline: 2px solid green; }' ]), // Add more information to this. Assertions.sAssertStructure( 'Initial structure of dropdown button', ApproxStructure.build((s, str, _arr) => s.element('button', { attrs: { 'aria-expanded': str.is('false'), 'aria-haspopup': str.is('true'), 'type': str.is('button') } })), component.element ), Mouse.sClickOn(gui.element, focusables.button.selector), FocusTools.sTryOnSelector('Focus should be on alpha', doc, 'li:contains(Alpha)'), Assertions.sAssertStructure( 'Initial structure of dropdown button', ApproxStructure.build((s, str, _arr) => s.element('button', { attrs: { 'aria-expanded': str.is('true'), 'aria-haspopup': str.is('true'), 'aria-controls': str.contains('aria-controls_') } })), component.element ), store.sClear, Chain.asStep(gui.element, [ UiFinder.cFindIn('.my-test-sandbox'), Chain.binder((sandbox) => component.getSystem().getByDom(sandbox)), Chain.op((sandboxComp) => { AlloyTriggers.emit(sandboxComp, 'made-up-event'); }), Chain.op((sandboxComp) => { Assertions.assertEq('Checking Representing.getValue of sandbox is dropdown', true, Compare.eq(component.element, Representing.getValue(sandboxComp).element)); }) ]), store.sAssertEq('Checking sandbox is getting event', [ 'received made-up-event' ]), store.sClear, Keyboard.sKeyup(doc, Keys.escape(), { }), Waiter.sTryUntil( 'Waiting for menu to disappear', FocusTools.sTryOnSelector('Focus should be back on button', doc, 'button') ), UiFinder.sNotExists(gui.element, '.menu'), Keyboard.sKeydown(doc, Keys.enter(), { }), Waiter.sTryUntil( 'Wait until dropdown content loads', // Just check the sandbox class is there. UiFinder.sExists(gui.element, '.my-test-sandbox .menu') ), FocusTools.sTryOnSelector( '1. focus should start on alpha', doc, focusables.alpha.selector ), DropdownAssertions.sSameWidth('Dropdown List', gui, component, '.menu'), NavigationUtils.sequence(doc, Keys.down(), {}, [ focusables.beta, focusables.gamma, focusables.delta, focusables.alpha ]), NavigationUtils.sequence(doc, Keys.up(), {}, [ focusables.delta, focusables.gamma, focusables.beta, focusables.alpha ]), NavigationUtils.sequence(doc, Keys.right(), {}, [ focusables.alpha, focusables.alpha ]), NavigationUtils.sequence(doc, Keys.left(), {}, [ focusables.alpha, focusables.alpha ]), NavigationUtils.sequence(doc, Keys.tab(), {}, [ focusables.beta, focusables.gamma, focusables.delta, focusables.alpha ]), NavigationUtils.sequence(doc, Keys.tab(), { shift: true }, [ focusables.delta, focusables.gamma, focusables.beta, focusables.alpha ]), Keyboard.sKeydown(doc, Keys.down(), {}), store.sAssertEq('nothing has been executed', [ ]), Keyboard.sKeydown(doc, Keys.enter(), {}), store.sAssertEq('after executing item: beta', [ 'dropdown.menu.execute: beta' ]), store.sClear, Keyboard.sKeydown(doc, Keys.down(), {}), Keyboard.sKeydown(doc, Keys.space(), {}), store.sAssertEq('after executing item: gamma', [ 'dropdown.menu.execute: gamma' ]), store.sClear, Mouse.sClickOn(gui.element, focusables.alpha.selector), store.sAssertEq('after executing item: alpha', [ 'dropdown.menu.execute: alpha' ]), store.sClear, Keyboard.sKeyup(doc, Keys.escape(), {}), FocusTools.sTryOnSelector( 'Focus should have moved back to the dropdown', doc, focusables.button.selector ), Mouse.sClickOn(gui.element, focusables.button.selector), FocusTools.sTryOnSelector( '2. focus should start on alpha', doc, focusables.alpha.selector ), TestBroadcasts.sDismiss( 'button: should not close', gui, component.element ), FocusTools.sIsOnSelector( 'focus should stay on alpha', doc, focusables.alpha.selector ), TestBroadcasts.sDismissOn( 'item: should not close', gui, focusables.alpha.selector ), FocusTools.sIsOnSelector( 'focus should stay on alpha after firing dismiss on alpha', doc, focusables.alpha.selector ), TestBroadcasts.sDismiss( 'outer element: should close', gui, gui.element ), Logger.t( 'After broadcasting dismiss popup on a non popup element, the menu should not longer exist in the DOM', UiFinder.sNotExists(gui.element, '.menu') ), Logger.t( 'Triggering the test-listener event should fire things to the store in the right order (1, 3, 2)', GeneralSteps.sequence([ store.sClear, Step.sync(() => AlloyTriggers.emit(component, 'test-listener')), store.sAssertEq( 'test-listener should respect eventOrder', [ 'test.listener.1', 'test.listener.3', 'test.listener.2' ] ) ]) ), Logger.t( 'Using the expand API should open the dropdown, without focusing / highlighting it', GeneralSteps.sequence([ store.sClear, Step.sync(() => { Focus.focus(component.element); }), Step.sync(() => { Dropdown.expand(component); }), UiFinder.sWaitForVisible('Waiting for menu to appear', sink.element, '.menu'), Step.sync(() => Assertions.assertEq('isOpen should be true', true, Dropdown.isOpen(component))), FocusTools.sTryOnSelector('Focus should still be on button (expand does not shift it)', doc, 'button') ]) ), Logger.t( 'Pressing in the button should focus the expanded menu', GeneralSteps.sequence([ Keyboard.sKeydown(doc, Keys.down(), { }), Step.sync(() => Assertions.assertEq('isOpen should be true', true, Dropdown.isOpen(component))), FocusTools.sTryOnSelector('Focus should still be on button (expand does not shift it)', doc, 'li:contains("Alpha")') ]) ), Logger.t( 'Closing the dropdown should hide the menu. Currently, it will not shift focus.', GeneralSteps.sequence([ Step.sync(() => { Focus.focus(inputForFocus.element); }), Step.sync(() => { Dropdown.close(component); }), UiFinder.sNotExists(sink.element, '.menu'), Step.sync(() => Assertions.assertEq('isOpen should be false', false, Dropdown.isOpen(component))), FocusTools.sTryOnSelector('Focus should not be shifted by the close call', doc, 'input') ]) ), Logger.t( 'Using the open API should open the dropdown, and focus / highlight the menu', GeneralSteps.sequence([ Step.sync(() => { Dropdown.open(component); }), UiFinder.sWaitForVisible('Waiting for menu to appear', sink.element, '.menu'), Step.sync(() => Assertions.assertEq('isOpen should be true', true, Dropdown.isOpen(component))), FocusTools.sTryOnSelector('Focus should still be on button (expand does not shift it)', doc, 'li:contains("Alpha")') ]) ), GuiSetup.mRemoveStyles ]; }, success, failure); });