diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b01b0e4..3de87a9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## v1.36.0 + +_11 Aug 2025_ + +- Added `passFocus` option to route options to prevent focus being passed to page navigated to +- Added missing type definitions for Route options +- Fixed sidebar in docs +- Added debug log messages to Announcer +- Added `remove()` function as (preferred) alias for announcer `message.cancel()` +- Fixed issue with removing a messages causing an interrupt of current message being read out + ## v1.35.5 _06 Aug 2025_ diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 90f6f5e7..77641bd7 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -33,5 +33,6 @@ - [Language](/plugins/language.md) - [Theme](/plugins/theme.md) - [Global App State](/plugins/global_app_state.md) + - [Storage](/plugins/storage.md) - Performance - [Lazy loading]('/performance/lazy-loading.md') diff --git a/docs/plugins/text-to-speech-announcer.md b/docs/plugins/text-to-speech-announcer.md index 1ad2da19..c3c907db 100644 --- a/docs/plugins/text-to-speech-announcer.md +++ b/docs/plugins/text-to-speech-announcer.md @@ -79,7 +79,7 @@ In some cases you may not want to clear the entire queue, but instead cancel out Imagine an App with a row of tiles, it's possible that before the title of the role is being spoken out, the user already navigates through the tiles within the row. Traditionally you'd use the focus event to speak out info about each tile (i.e. adding tot the queue). You don't want all previously focused tiles to still be announced, but would still want the category of the row to be announced, making clearing the queue not required. -The `speak()`-method return a Promise that also contains a `cancel()` function. When called, it will cancel that specific message and remove it from the queue before it can be spoken out. +The `speak()`-method return a Promise that also contains a `remove()` function. When called, it will remove it from the queue before it can be spoken out. Additionally if you want to _interrupt_ a specific messages as it's being spoken out as well and go straight to the next message in the queue (i.e. the newly focused item, for example). You can use the `stop()` message that is returned on the Promise returned by the `speak()`-method. @@ -100,8 +100,8 @@ Blits.Component('MyTile', { unfocus() { // when unfocused interrupt the message if it's already being spoken out this.message.stop() - // and cancel the message to remove it from the queue - this.message.cancel() + // and remove the message from the queue + this.message.remove() } } }) @@ -117,4 +117,4 @@ Alternatively the announcer can be enabled or disabled run time by using one of - `this.$announcer.enable()` - activates the announcer - `this.$announcer.disable()` - deactivates the announcer -- `this.$announcer.disable(true/false)` - turns the announcer or on off \ No newline at end of file +- `this.$announcer.disable(true/false)` - turns the announcer on or off diff --git a/docs/router/basics.md b/docs/router/basics.md index c25279f1..1576c1be 100644 --- a/docs/router/basics.md +++ b/docs/router/basics.md @@ -96,6 +96,8 @@ export default Blits.Component('Poster', { Whenever you navigate to a new page, the URL hash will automatically be updated. Unless specified otherwise, navigating to a new page, will add that route to the history stack. The `back` input action is automatically wired up to navigate back down the history stack. +By default, every time you navigate to a new route, the application focus will be automatically passed to the newly loaded page. If you instead want to maintain the current focus (for example in a widget that sits above your RouterView), you can use `passFocus: false` as part of the router options. + ## Deeplinking The Router plugin has support for deeplinking. When the App is loaded with a URL hash (i.e. `#/pages/settings/network`), the router will try to match that hash to a defined route. This means that your app can be deep linked into, by simply providing the correct URL hash. diff --git a/docs/sidebar.json b/docs/sidebar.json index d0bb88f4..2942c487 100644 --- a/docs/sidebar.json +++ b/docs/sidebar.json @@ -112,15 +112,6 @@ } ] }, - { - "text": "Performance", - "items": [ - { - "text": "Lazy Loading", - "link": "/performance/lazy-loading" - } - ] - }, { "text": "Router", "items": [ @@ -156,6 +147,10 @@ { "text": "Global App State", "link": "/plugins/global_app_state" + }, + { + "text": "Storage", + "link": "/plugins/storage" } ] }, diff --git a/index.d.ts b/index.d.ts index 2903e4c3..c4f49979 100644 --- a/index.d.ts +++ b/index.d.ts @@ -187,7 +187,30 @@ declare module '@lightningjs/blits' { // todo: specify valid route options export interface RouteOptions { - [key: string]: any + /** + * Whether the page navigation should be added to the history stack + * used when navigating back using `this.$router.back()` + * + * @default true + */ + inHistory?: Boolean + /** + * Whether the page should be kept alive when navigating away. Can be useful + * for a homepage where the state should be fully retained when navigating back + * from a details page + * + * @default false + */ + keepAlive?: Boolean + /** + * Whether the focus should be delegated to the page that's being navigated to. + * Can be useful when navigating to a new page from a widget / menu overlaying the + * RouterView, where the widget should maintain the focus (instead of the new page, which + * is the default behaviour) + * + * @default true + */ + passFocus?: Boolean } export interface Router { @@ -628,7 +651,7 @@ declare module '@lightningjs/blits' { /** * Extra route options */ - options?: object // todo: specify which options are available, + options?: RouteOptions /** * Message to be announced when visiting the route (often used for accessibility purposes) * diff --git a/package-lock.json b/package-lock.json index d7003bd6..5844b86e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lightningjs/blits", - "version": "1.35.5", + "version": "1.36.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@lightningjs/blits", - "version": "1.35.5", + "version": "1.36.0", "license": "Apache-2.0", "dependencies": { "@lightningjs/msdf-generator": "^1.1.1", diff --git a/package.json b/package.json index e1b392ad..115513a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lightningjs/blits", - "version": "1.35.5", + "version": "1.36.0", "description": "Blits: The Lightning 3 App Development Framework", "bin": "bin/index.js", "exports": { diff --git a/src/announcer/announcer.js b/src/announcer/announcer.js index ddecd7b7..01ac6df6 100644 --- a/src/announcer/announcer.js +++ b/src/announcer/announcer.js @@ -15,6 +15,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { Log } from '../lib/log.js' import speechSynthesis from './speechSynthesis.js' let active = false @@ -28,6 +29,7 @@ const noopAnnouncement = { then() {}, done() {}, cancel() {}, + remove() {}, stop() {}, } @@ -66,11 +68,11 @@ const addToQueue = (message, politeness, delay = false) => { resolveFn = resolve }) - // augment the promise with a cancel function - done.cancel = () => { + // augment the promise with a cancel / remove function + done.remove = done.cancel = () => { const index = queue.findIndex((item) => item.id === id) if (index !== -1) queue.splice(index, 1) - isProcessing = false + Log.debug(`Announcer - removed from queue: "${message}" (id: ${id})`) resolveFn('canceled') } @@ -92,6 +94,8 @@ const addToQueue = (message, politeness, delay = false) => { queue.push({ delay, resolveFn, id }) } + Log.debug(`Announcer - added to queue: "${message}" (id: ${id})`) + setTimeout(() => { processQueue() }, 100) @@ -118,17 +122,22 @@ const processQueue = async () => { if (debounce !== null) clearTimeout(debounce) // add some easing when speaking the messages to reduce stuttering debounce = setTimeout(() => { + Log.debug(`Announcer - speaking: "${message}" (id: ${id})`) + speechSynthesis - .speak({ message }) + .speak({ message, id }) .then(() => { - isProcessing = false + Log.debug(`Announcer - finished speaking: "${message}" (id: ${id})`) + currentId = null + isProcessing = false resolveFn('finished') processQueue() }) .catch((e) => { - isProcessing = false currentId = null + isProcessing = false + Log.debug(`Announcer - error ("${e.error}") while speaking: "${message}" (id: ${id})`) resolveFn(e.error) processQueue() }) diff --git a/src/announcer/speechSynthesis.js b/src/announcer/speechSynthesis.js index 697a8356..5c454ba4 100644 --- a/src/announcer/speechSynthesis.js +++ b/src/announcer/speechSynthesis.js @@ -64,7 +64,7 @@ const initialize = () => { const speak = (options) => { const utterance = new SpeechSynthesisUtterance(options.message) - const id = Date.now() + Math.random() // Unique ID for tracking + const id = options.id utterance.lang = options.lang || defaultUtteranceProps.lang utterance.pitch = options.pitch || defaultUtteranceProps.pitch utterance.rate = options.rate || defaultUtteranceProps.rate diff --git a/src/router/router.js b/src/router/router.js index 3e7c0a9c..dee317b1 100644 --- a/src/router/router.js +++ b/src/router/router.js @@ -340,8 +340,10 @@ export const navigate = async function () { const children = this[symbols.children] this.activeView = children[children.length - 1] - // set focus to the view that we're routing to - focus ? focus.$focus() : /** @type {BlitsComponent} */ (view).$focus() + // set focus to the view that we're routing to (unless explicitly disabling passing focus) + if (route.options.passFocus !== false) { + focus ? focus.$focus() : /** @type {BlitsComponent} */ (view).$focus() + } // apply before settings to holder element if (route.transition.before) {