@lightningjs/blits 1.35.5 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.37.0
4
+
5
+ _22 Aug 2025_
6
+
7
+ - Added Route options check before checking passFocus flag
8
+ - Added support for additional metadata in Route config (for associating arbitrary data to a route)
9
+ - Upgraded dev dependencies to latest versions
10
+
11
+ ## v1.36.0
12
+
13
+ _11 Aug 2025_
14
+
15
+ - Added `passFocus` option to route options to prevent focus being passed to page navigated to
16
+ - Added missing type definitions for Route options
17
+ - Fixed sidebar in docs
18
+ - Added debug log messages to Announcer
19
+ - Added `remove()` function as (preferred) alias for announcer `message.cancel()`
20
+ - Fixed issue with removing a messages causing an interrupt of current message being read out
21
+
3
22
  ## v1.35.5
4
23
 
5
24
  _06 Aug 2025_
@@ -0,0 +1,85 @@
1
+ /*
2
+ * Copyright 2023 Comcast Cable Communications Management, LLC
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License.
5
+ * You may obtain a copy of the License at
6
+ *
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software
10
+ * distributed under the License is distributed on an "AS IS" BASIS,
11
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ * See the License for the specific language governing permissions and
13
+ * limitations under the License.
14
+ *
15
+ * SPDX-License-Identifier: Apache-2.0
16
+ */
17
+
18
+ const {
19
+ defineConfig,
20
+ globalIgnores,
21
+ } = require("eslint/config");
22
+
23
+ const globals = require("globals");
24
+ const prettier = require("eslint-plugin-prettier");
25
+ const babelParser = require("@babel/eslint-parser");
26
+ const js = require("@eslint/js");
27
+
28
+ const {
29
+ FlatCompat,
30
+ } = require("@eslint/eslintrc");
31
+
32
+ const compat = new FlatCompat({
33
+ baseDirectory: __dirname,
34
+ recommendedConfig: js.configs.recommended,
35
+ allConfig: js.configs.all
36
+ });
37
+
38
+ module.exports = defineConfig([{
39
+ languageOptions: {
40
+ globals: {
41
+ ...globals.browser,
42
+ ...globals.node,
43
+ globalThis: false,
44
+ },
45
+
46
+ parser: babelParser,
47
+ ecmaVersion: 2018,
48
+ sourceType: "module",
49
+
50
+ parserOptions: {
51
+ requireConfigFile: false,
52
+
53
+ babelOptions: {
54
+ plugins: ["@babel/plugin-syntax-import-assertions"],
55
+ },
56
+ },
57
+ },
58
+
59
+ plugins: {
60
+ prettier,
61
+ },
62
+
63
+ extends: compat.extends("eslint:recommended", "plugin:prettier/recommended", "prettier"),
64
+
65
+ rules: {
66
+ "no-console": process.env.NODE_ENV === "production" ? "error" : "off",
67
+ "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
68
+ quotes: [2, "single", "avoid-escape"],
69
+ semi: [2, "never"],
70
+ "no-extra-boolean-cast": "off",
71
+
72
+ "no-unused-vars": [1, {
73
+ argsIgnorePattern: "res|next|^err",
74
+ }],
75
+
76
+ "prettier/prettier": ["error", {
77
+ trailingComma: "es5",
78
+ singleQuote: true,
79
+ tabWidth: 2,
80
+ semi: false,
81
+ printWidth: 100,
82
+ endOfLine: "auto",
83
+ }],
84
+ },
85
+ }, globalIgnores(["**/node_modules"])]);
package/index.d.ts CHANGED
@@ -187,7 +187,30 @@ declare module '@lightningjs/blits' {
187
187
 
188
188
  // todo: specify valid route options
189
189
  export interface RouteOptions {
190
- [key: string]: any
190
+ /**
191
+ * Whether the page navigation should be added to the history stack
192
+ * used when navigating back using `this.$router.back()`
193
+ *
194
+ * @default true
195
+ */
196
+ inHistory?: Boolean
197
+ /**
198
+ * Whether the page should be kept alive when navigating away. Can be useful
199
+ * for a homepage where the state should be fully retained when navigating back
200
+ * from a details page
201
+ *
202
+ * @default false
203
+ */
204
+ keepAlive?: Boolean
205
+ /**
206
+ * Whether the focus should be delegated to the page that's being navigated to.
207
+ * Can be useful when navigating to a new page from a widget / menu overlaying the
208
+ * RouterView, where the widget should maintain the focus (instead of the new page, which
209
+ * is the default behaviour)
210
+ *
211
+ * @default true
212
+ */
213
+ passFocus?: Boolean
191
214
  }
192
215
 
193
216
  export interface Router {
@@ -628,7 +651,7 @@ declare module '@lightningjs/blits' {
628
651
  /**
629
652
  * Extra route options
630
653
  */
631
- options?: object // todo: specify which options are available,
654
+ options?: RouteOptions
632
655
  /**
633
656
  * Message to be announced when visiting the route (often used for accessibility purposes)
634
657
  *
@@ -656,6 +679,17 @@ declare module '@lightningjs/blits' {
656
679
  data?: {
657
680
  [key: string]: any
658
681
  }
682
+
683
+ /**
684
+ * Metadata attached to the route, can be used to add any arbitrary
685
+ * data to a route such as `auth: true/false`, a route ID or route description
686
+ * Note that this data is not reactive and not passed as props to components.
687
+ * The metadata is available in the router `before` or `beforeEach` hooks
688
+ *
689
+ */
690
+ meta?: {
691
+ [key: string]: any
692
+ }
659
693
  }
660
694
 
661
695
  type ComponentFactory = () => void
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "1.35.5",
3
+ "version": "1.37.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -35,16 +35,19 @@
35
35
  "devDependencies": {
36
36
  "@babel/eslint-parser": "^7.26.5",
37
37
  "@babel/plugin-syntax-import-assertions": "^7.26.0",
38
- "c8": "^8.0.1",
39
- "eslint": "^8.8.0",
40
- "eslint-config-prettier": "^8.3.0",
41
- "eslint-plugin-prettier": "^4.0.0",
38
+ "@eslint/eslintrc": "^3.3.1",
39
+ "@eslint/js": "^9.33.0",
40
+ "c8": "^9.1.0",
41
+ "eslint": "^9.33.0",
42
+ "eslint-config-prettier": "^10.1.8",
43
+ "eslint-plugin-prettier": "^5.5.4",
42
44
  "fast-glob": "^3.3.3",
43
45
  "global-jsdom": "24.0.0",
46
+ "globals": "^16.3.0",
44
47
  "husky": "^9.1.7",
45
48
  "jsdom": "24.0.0",
46
49
  "lint-staged": "^15.5.0",
47
- "prettier": "^2.5.1",
50
+ "prettier": "^3.6.2",
48
51
  "sinon": "^21.0.0",
49
52
  "tap-diff": "^0.1.1",
50
53
  "tape": "^5.5.0"
@@ -15,6 +15,7 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
+ import { Log } from '../lib/log.js'
18
19
  import speechSynthesis from './speechSynthesis.js'
19
20
 
20
21
  let active = false
@@ -28,6 +29,7 @@ const noopAnnouncement = {
28
29
  then() {},
29
30
  done() {},
30
31
  cancel() {},
32
+ remove() {},
31
33
  stop() {},
32
34
  }
33
35
 
@@ -66,11 +68,11 @@ const addToQueue = (message, politeness, delay = false) => {
66
68
  resolveFn = resolve
67
69
  })
68
70
 
69
- // augment the promise with a cancel function
70
- done.cancel = () => {
71
+ // augment the promise with a cancel / remove function
72
+ done.remove = done.cancel = () => {
71
73
  const index = queue.findIndex((item) => item.id === id)
72
74
  if (index !== -1) queue.splice(index, 1)
73
- isProcessing = false
75
+ Log.debug(`Announcer - removed from queue: "${message}" (id: ${id})`)
74
76
  resolveFn('canceled')
75
77
  }
76
78
 
@@ -92,6 +94,8 @@ const addToQueue = (message, politeness, delay = false) => {
92
94
  queue.push({ delay, resolveFn, id })
93
95
  }
94
96
 
97
+ Log.debug(`Announcer - added to queue: "${message}" (id: ${id})`)
98
+
95
99
  setTimeout(() => {
96
100
  processQueue()
97
101
  }, 100)
@@ -118,17 +122,22 @@ const processQueue = async () => {
118
122
  if (debounce !== null) clearTimeout(debounce)
119
123
  // add some easing when speaking the messages to reduce stuttering
120
124
  debounce = setTimeout(() => {
125
+ Log.debug(`Announcer - speaking: "${message}" (id: ${id})`)
126
+
121
127
  speechSynthesis
122
- .speak({ message })
128
+ .speak({ message, id })
123
129
  .then(() => {
124
- isProcessing = false
130
+ Log.debug(`Announcer - finished speaking: "${message}" (id: ${id})`)
131
+
125
132
  currentId = null
133
+ isProcessing = false
126
134
  resolveFn('finished')
127
135
  processQueue()
128
136
  })
129
137
  .catch((e) => {
130
- isProcessing = false
131
138
  currentId = null
139
+ isProcessing = false
140
+ Log.debug(`Announcer - error ("${e.error}") while speaking: "${message}" (id: ${id})`)
132
141
  resolveFn(e.error)
133
142
  processQueue()
134
143
  })
@@ -64,7 +64,7 @@ const initialize = () => {
64
64
 
65
65
  const speak = (options) => {
66
66
  const utterance = new SpeechSynthesisUtterance(options.message)
67
- const id = Date.now() + Math.random() // Unique ID for tracking
67
+ const id = options.id
68
68
  utterance.lang = options.lang || defaultUtteranceProps.lang
69
69
  utterance.pitch = options.pitch || defaultUtteranceProps.pitch
70
70
  utterance.rate = options.rate || defaultUtteranceProps.rate
@@ -96,9 +96,12 @@ const Application = (config) => {
96
96
 
97
97
  Focus.input(key, e)
98
98
  clearTimeout(holdTimeout)
99
- holdTimeout = setTimeout(() => {
100
- Focus.hold = true
101
- }, Settings.get('holdTimeout', DEFAULT_HOLD_TIMEOUT_MS))
99
+ holdTimeout = setTimeout(
100
+ () => {
101
+ Focus.hold = true
102
+ },
103
+ Settings.get('holdTimeout', DEFAULT_HOLD_TIMEOUT_MS)
104
+ )
102
105
  }
103
106
 
104
107
  keyUpHandler = (e) => {
@@ -41,8 +41,8 @@ export default (component, props = []) => {
41
41
  this[symbols.props] !== undefined && prop.key in this[symbols.props]
42
42
  ? this[symbols.props][prop.key]
43
43
  : 'default' in prop
44
- ? prop.default
45
- : undefined
44
+ ? prop.default
45
+ : undefined
46
46
  )
47
47
 
48
48
  if (prop.required === true && value === undefined) {
@@ -107,8 +107,8 @@ const layoutFn = function (config) {
107
107
  ? node.height + gap
108
108
  : 0
109
109
  : node.height !== 0
110
- ? node.height + gap
111
- : 0
110
+ ? node.height + gap
111
+ : 0
112
112
  }
113
113
  otherDimension = Math.max(
114
114
  otherDimension,
@@ -191,9 +191,9 @@ const generateElementCode = function (
191
191
  } else {
192
192
  this.effectsCode.push(`
193
193
  ${elm}.set('${key.substring(1)}', ${interpolate(
194
- templateObject[key],
195
- options.component
196
- )})
194
+ templateObject[key],
195
+ options.component
196
+ )})
197
197
  `)
198
198
  }
199
199
  // value.includes('.') === false &&
@@ -310,9 +310,9 @@ const generateComponentCode = function (
310
310
  if (isReactiveKey(key)) {
311
311
  this.effectsCode.push(`
312
312
  ${elm}[Symbol.for('props')]['${key.substring(1)}'] = ${interpolate(
313
- templateObject[key],
314
- options.component
315
- )}`)
313
+ templateObject[key],
314
+ options.component
315
+ )}`)
316
316
  renderCode.push(`
317
317
  propData = ${interpolate(templateObject[key], options.component)}
318
318
  if (Array.isArray(propData) === true) {
@@ -41,8 +41,8 @@ export default {
41
41
  Object.keys(translations).length === 0
42
42
  ? 'No translations loaded. Please load a file with translations or specify a translations object manually'
43
43
  : language in translations === false
44
- ? `Language ${language} not available in the loaded translations`
45
- : false
44
+ ? `Language ${language} not available in the loaded translations`
45
+ : false
46
46
  if (warningMsg) {
47
47
  Log.warn(warningMsg)
48
48
  // set the current language in the reactive state
@@ -127,12 +127,15 @@ export default {
127
127
  return Object.keys(
128
128
  // maps array input to an object {0: 'item1', 1: 'item2'}
129
129
  Array.isArray(replacements) ? Object.assign({}, replacements) : replacements
130
- ).reduce((text, replacementKey) => {
131
- return text.replace(
132
- new RegExp('{\\s?' + replacementKey + '\\s?}', 'g'),
133
- replacements[replacementKey]
134
- )
135
- }, (dictionary && dictionary[key]) || key)
130
+ ).reduce(
131
+ (text, replacementKey) => {
132
+ return text.replace(
133
+ new RegExp('{\\s?' + replacementKey + '\\s?}', 'g'),
134
+ replacements[replacementKey]
135
+ )
136
+ },
137
+ (dictionary && dictionary[key]) || key
138
+ )
136
139
  }
137
140
  },
138
141
  get language() {
@@ -340,8 +340,10 @@ export const navigate = async function () {
340
340
  const children = this[symbols.children]
341
341
  this.activeView = children[children.length - 1]
342
342
 
343
- // set focus to the view that we're routing to
344
- focus ? focus.$focus() : /** @type {BlitsComponent} */ (view).$focus()
343
+ // set focus to the view that we're routing to (unless explicitly disabling passing focus)
344
+ if (route.options === undefined || route.options.passFocus !== false) {
345
+ focus ? focus.$focus() : /** @type {BlitsComponent} */ (view).$focus()
346
+ }
345
347
 
346
348
  // apply before settings to holder element
347
349
  if (route.transition.before) {