From 7afd213740aa3c6651df30cc9e92f67c30c3766c Mon Sep 17 00:00:00 2001 From: Jonathan Olson Date: Wed, 26 Oct 2022 13:45:08 -0600 Subject: [PATCH] Adding globalkeydown/globalkeyup input events, see https://github.com/phetsims/scenery/issues/1445 --- js/input/Input.ts | 70 +++++++++++++++++++++++++++++++------- js/input/TInputListener.ts | 4 +++ 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/js/input/Input.ts b/js/input/Input.ts index c3428d7b7..042d263c0 100644 --- a/js/input/Input.ts +++ b/js/input/Input.ts @@ -109,6 +109,11 @@ * - keyup : Triggered for all keys when released. When a screen reader is active, this event will be omitted * role="button" is activated. * See https://www.w3.org/TR/DOM-Level-3-Events/#keyup + * - globalkeydown: Triggered for all keys pressed, regardless of whether the Node has focus. It just needs to be + * visible, inputEnabled, and all of its ancestors visible and inputEnabled. + * - globalkeyup: Triggered for all keys released, regardless of whether the Node has focus. It just needs to be + * visible, inputEnabled, and all of its ancestors visible and inputEnabled. + * * * *** Event Dispatch * @@ -841,11 +846,23 @@ export default class Input extends PhetioObject { const notBlockingSubsequentClicksOccurringTooQuickly = trail && !( eventName === 'click' && _.some( trail.nodes, node => node.positionInPDOM ) && event.timeStamp - this.upTimeStamp <= PDOM_CLICK_DELAY ); - + if ( eventName === 'keydown' ) { + this.dispatchGlobalEvent( 'globalkeydown', event as KeyboardEvent, true ); + } + if ( eventName === 'keyup' ) { + this.dispatchGlobalEvent( 'globalkeyup', event as KeyboardEvent, true ); + } if ( trail && notBlockingSubsequentClicksOccurringTooQuickly ) { ( this[ actionName as keyof Input ] as unknown as PhetioAction<[ Event ]> ).execute( event ); } + + if ( eventName === 'keydown' ) { + this.dispatchGlobalEvent( 'globalkeydown', event as KeyboardEvent, false ); + } + if ( eventName === 'keyup' ) { + this.dispatchGlobalEvent( 'globalkeyup', event as KeyboardEvent, false ); + } } sceneryLog && sceneryLog.InputEvent && sceneryLog.pop(); @@ -1136,6 +1153,31 @@ export default class Input extends PhetioObject { } } + private dispatchGlobalEvent( eventType: string, domEvent: DOMEvent, capture: boolean ): void { + + this.ensurePDOMPointer(); + assert && assert( this.pdomPointer ); + const pointer = this.pdomPointer!; + const inputEvent = new SceneryEvent( new Trail(), eventType, pointer, domEvent ); + + const recursiveGlobalDispatch = ( node: Node ) => { + if ( !node.isDisposed && node.isVisible() && node.isInputEnabled() ) { + // Reverse iteration follows the z-order from "visually in front" to "visually in back" like normal dipatch + for ( let i = node._children.length - 1; i >= 0; i-- ) { + recursiveGlobalDispatch( node._children[ i ] ); + } + + if ( !inputEvent.aborted && !inputEvent.handled ) { + // Notification of ourself AFTER our children results in the depth-first scan. + inputEvent.currentTarget = node; + this.dispatchToListeners( pointer, node._inputListeners, eventType, inputEvent, capture ); + } + } + }; + + recursiveGlobalDispatch( this.rootNode ); + } + /** * From a DOM Event, get its relatedTarget and map that to the scenery Node. Will return null if relatedTarget * is not provided, or if relatedTarget is not under PDOM, or there is no associated Node with trail id on the @@ -1781,7 +1823,7 @@ export default class Input extends PhetioObject { * @param type * @param inputEvent */ - private dispatchToListeners( pointer: Pointer, listeners: TInputListener[], type: string, inputEvent: SceneryEvent ): void { + private dispatchToListeners( pointer: Pointer, listeners: TInputListener[], type: string, inputEvent: SceneryEvent, capture: boolean | null = null ): void { if ( inputEvent.handled ) { return; @@ -1792,22 +1834,24 @@ export default class Input extends PhetioObject { for ( let i = 0; i < listeners.length; i++ ) { const listener = listeners[ i ]; - if ( !inputEvent.aborted && listener[ specificType as keyof TInputListener ] ) { - sceneryLog && sceneryLog.EventDispatch && sceneryLog.EventDispatch( specificType ); - sceneryLog && sceneryLog.EventDispatch && sceneryLog.push(); + if ( capture === null || capture === !!listener.capture ) { + if ( !inputEvent.aborted && listener[ specificType as keyof TInputListener ] ) { + sceneryLog && sceneryLog.EventDispatch && sceneryLog.EventDispatch( specificType ); + sceneryLog && sceneryLog.EventDispatch && sceneryLog.push(); - ( listener[ specificType as keyof TInputListener ] as SceneryListenerFunction )( inputEvent ); + ( listener[ specificType as keyof TInputListener ] as SceneryListenerFunction )( inputEvent ); - sceneryLog && sceneryLog.EventDispatch && sceneryLog.pop(); - } + sceneryLog && sceneryLog.EventDispatch && sceneryLog.pop(); + } - if ( !inputEvent.aborted && listener[ type as keyof TInputListener ] ) { - sceneryLog && sceneryLog.EventDispatch && sceneryLog.EventDispatch( type ); - sceneryLog && sceneryLog.EventDispatch && sceneryLog.push(); + if ( !inputEvent.aborted && listener[ type as keyof TInputListener ] ) { + sceneryLog && sceneryLog.EventDispatch && sceneryLog.EventDispatch( type ); + sceneryLog && sceneryLog.EventDispatch && sceneryLog.push(); - ( listener[ type as keyof TInputListener ] as SceneryListenerFunction )( inputEvent ); + ( listener[ type as keyof TInputListener ] as SceneryListenerFunction )( inputEvent ); - sceneryLog && sceneryLog.EventDispatch && sceneryLog.pop(); + sceneryLog && sceneryLog.EventDispatch && sceneryLog.pop(); + } } } } diff --git a/js/input/TInputListener.ts b/js/input/TInputListener.ts index c3b478e11..3ae529b1b 100644 --- a/js/input/TInputListener.ts +++ b/js/input/TInputListener.ts @@ -14,6 +14,7 @@ export type SceneryListenerFunction = ( event: SceneryE type TInputListener = { interrupt?: () => void; cursor?: string | null; + capture?: boolean; // NOTE: only applies to globalkeydown / globalkeyup focus?: SceneryListenerFunction; blur?: SceneryListenerFunction; @@ -23,6 +24,9 @@ type TInputListener = { keydown?: SceneryListenerFunction; keyup?: SceneryListenerFunction; + globalkeydown?: SceneryListenerFunction; + globalkeyup?: SceneryListenerFunction; + click?: SceneryListenerFunction; input?: SceneryListenerFunction; change?: SceneryListenerFunction;