diff --git a/CHANGELOG.md b/CHANGELOG.md index a87efad..62033ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog for apl-viewhost-web +## [1.7.1] + +## Changed + +- Fixed scrolling issue with SpeakItem command when highlight mode set to line + ## [1.7.0] This release adds support for version 1.7 of the APL specification. Please also see APL Core Library for changes: [apl-core-library CHANGELOG](https://github.com/alexa/apl-core-library/blob/master/CHANGELOG.md) diff --git a/js/apl-html/src/APLRenderer.ts b/js/apl-html/src/APLRenderer.ts index c6a16f6..af27815 100644 --- a/js/apl-html/src/APLRenderer.ts +++ b/js/apl-html/src/APLRenderer.ts @@ -452,7 +452,8 @@ export default abstract class APLRenderer { public init(metricRecorder?: (m: APL.DisplayMetric) => void) { const startTime = performance.now(); if (this.mOptions.mode === 'TV') { - window.addEventListener('keydown', this.passWindowEventsToCore); + window.addEventListener('keydown', this.passKeyDownToCore); + window.addEventListener('keyup', this.passKeyUpToCore); } this.renderComponents(); const stopTime = performance.now(); @@ -713,7 +714,8 @@ export default abstract class APLRenderer { } this.view = undefined; } - window.removeEventListener('keydown', this.passWindowEventsToCore); + window.removeEventListener('keydown', this.passKeyDownToCore); + window.removeEventListener('keyup', this.passKeyUpToCore); } /** @@ -1040,12 +1042,20 @@ export default abstract class APLRenderer { } } + private canPassLocalKeyDown = (event: IAsyncKeyboardEvent) => { + return this.mOptions.mode !== 'TV' || !this.isDPadKey(event.code); + } + private handleKeyDown = async (evt: IAsyncKeyboardEvent) => { - await this.passKeyboardEventToCore(evt, KeyHandlerType.KeyDown); + if (this.canPassLocalKeyDown(evt)) { + await this.passKeyboardEventToCore(evt, KeyHandlerType.KeyDown); + } } private handleKeyUp = async (evt: IAsyncKeyboardEvent) => { - await this.passKeyboardEventToCore(evt, KeyHandlerType.KeyUp); + if (this.canPassLocalKeyDown(evt)) { + await this.passKeyboardEventToCore(evt, KeyHandlerType.KeyUp); + } } /** @@ -1133,28 +1143,50 @@ export default abstract class APLRenderer { } } - private recoverFocusOnEnter(id: string, code: string): void { - if (code === ENTER_KEY) { - const component = this.componentMap[id] as ActionableComponent; - if (component['focus']) { - component.focus(); - } - } + private passKeyDownToCore = (event: IAsyncKeyboardEvent) => { + this.passWindowEventsToCore(event, KeyHandlerType.KeyDown); } - private passWindowEventsToCore = async (event: IAsyncKeyboardEvent) => { + private passKeyUpToCore = (event: IAsyncKeyboardEvent) => { + this.passWindowEventsToCore(event, KeyHandlerType.KeyUp); + } + + private passWindowEventsToCore = async (event: IAsyncKeyboardEvent, handler: KeyHandlerType) => { if (!this.context) { return; } - const focused = await this.context.getFocused(); - if (this.isDPadKey(event.code) - && (!document.activeElement || document.activeElement === document.body) - && focused) { - this.recoverFocusOnEnter(focused, event.code); - this.passKeyboardEventToCore(event, KeyHandlerType.KeyDown); - } else if (!focused) { + const focusedComponentId = await this.context.getFocused(); + + if (this.shouldPassWindowEventToCore(event, focusedComponentId)) { + this.ensureComponentIsFocused(focusedComponentId, event.code); + this.passKeyboardEventToCore(event, handler); + } else if (!focusedComponentId) { this.focusTopLeft(); } } + + private shouldPassWindowEventToCore(event: IAsyncKeyboardEvent, focusedComponentId: string) { + const isViewAlreadyFocused = () => { + return this.view.contains(document.activeElement); + }; + + const isFocusLost = () => { + return !this.view.contains(document.activeElement) + && !(document.activeElement instanceof HTMLTextAreaElement); + }; + + return this.isDPadKey(event.code) + && focusedComponentId + && (isFocusLost() || isViewAlreadyFocused()); + } + + private ensureComponentIsFocused(id: string, code: string): void { + if (code === ENTER_KEY) { + const component = this.componentMap[id] as ActionableComponent; + if (component['focus']) { + component.focus(); + } + } + } } diff --git a/js/apl-html/src/events/RequestFirstLineBounds.ts b/js/apl-html/src/events/RequestFirstLineBounds.ts index 3b34383..ad8020e 100644 --- a/js/apl-html/src/events/RequestFirstLineBounds.ts +++ b/js/apl-html/src/events/RequestFirstLineBounds.ts @@ -23,6 +23,8 @@ export class RequestFirstLineBounds extends Event { // Highlight first line as this event applicable only to line highlighting. this.component.highlight(0); this.event.resolveWithRect(top, 0, this.component.bounds.width, height); + } else if (this.component === undefined) { + this.event.resolveWithRect(0, 0, 0, 0); } } } diff --git a/js/apl-html/src/media/audio/AudioPlayer.ts b/js/apl-html/src/media/audio/AudioPlayer.ts index edfe63b..01c96d1 100644 --- a/js/apl-html/src/media/audio/AudioPlayer.ts +++ b/js/apl-html/src/media/audio/AudioPlayer.ts @@ -116,15 +116,15 @@ export abstract class AudioPlayer { } const onDecode = (audioBuffer: AudioBuffer) => { - const audioNode = this.getAudioNode(audioContext); + const audioNode = this.getConnectedAudioNode(audioContext); this.currentSource = audioContext.createBufferSource(); this.currentSource.buffer = audioBuffer; - audioNode.connect(audioContext.destination); this.currentSource.connect(audioNode); this.currentSource.onended = (event: Event) => { this.currentSource.disconnect(); audioNode.disconnect(); + this._audioNode = null; this.currentSource = null; this.onPlaybackFinished(id); this.resourceMap.delete(id); @@ -145,14 +145,18 @@ export abstract class AudioPlayer { onDecodeError); } - // The gainNode passed in should be connected to the audiocontext destination + // The AudioNode passed in should be connected to the AudioContext destination protected setCurrentAudioNode(node: IAudioNode): void { this.disconnectCurrentAudioNode(); this._audioNode = node; } - private getAudioNode(context: AudioContext): IAudioNode { - this._audioNode = this._audioNode || context.createGain(); + // Gets an AudioNode connected to the AudioContext destination + private getConnectedAudioNode(context: AudioContext): IAudioNode { + if (!this._audioNode) { + this._audioNode = context.createGain(); + this._audioNode.connect(context.destination); + } return this._audioNode; } diff --git a/js/package.json b/js/package.json index 39803bd..dc14f43 100644 --- a/js/package.json +++ b/js/package.json @@ -23,6 +23,7 @@ "clean:html": "cd apl-html && yarn clean", "clean:wasm": "cd apl-wasm && yarn clean", "clean:dts-packer": "cd dts-packer && yarn clean", - "clean": "rimraf node_modules && yarn clean:client && yarn clean:html && yarn clean:wasm && yarn clean:dts-packer" + "clean": "rimraf node_modules && yarn clean:client && yarn clean:html && yarn clean:wasm && yarn clean:dts-packer", + "lint": "yarn workspaces run lint" } } diff --git a/package.json b/package.json index e55057b..0c677ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "apl-viewhost-web", - "version": "1.7.0", + "version": "1.7.1", "description": "This is a Web-assembly version (WASM) of apl viewhost web.", "license": "Apache 2.0", "repository": { diff --git a/scripts/fetch.js b/scripts/fetch.js index 6fe4075..c2134a2 100644 --- a/scripts/fetch.js +++ b/scripts/fetch.js @@ -3,7 +3,7 @@ const https = require('https'); const fs = require('fs'); -const artifactUrl = 'https://d1gkjrhppbyzyh.cloudfront.net/apl-viewhost-web/fc1aa3e9-d453-46c4-919f-82b4274a2c68/index.js'; +const artifactUrl = 'https://d1gkjrhppbyzyh.cloudfront.net/apl-viewhost-web/EFB6DBCC-A601-46D7-8F05-06D91E9A7E09/index.js'; const outputFilePath = 'index.js'; const outputFile = fs.createWriteStream(outputFilePath);