@lightningtv/solid 3.0.0-19 → 3.0.0-20

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.
Files changed (36) hide show
  1. package/dist/src/primitives/Lazy.d.ts +1 -0
  2. package/dist/src/primitives/Lazy.jsx +14 -1
  3. package/dist/src/primitives/Lazy.jsx.map +1 -1
  4. package/dist/src/primitives/Virtual.jsx +7 -1
  5. package/dist/src/primitives/Virtual.jsx.map +1 -1
  6. package/dist/src/primitives/announcer/announcer.d.ts +1 -0
  7. package/dist/src/primitives/announcer/announcer.js +4 -3
  8. package/dist/src/primitives/announcer/announcer.js.map +1 -1
  9. package/dist/src/primitives/announcer/speech.d.ts +1 -1
  10. package/dist/src/primitives/announcer/speech.js +98 -8
  11. package/dist/src/primitives/announcer/speech.js.map +1 -1
  12. package/dist/src/primitives/createTag.d.ts +8 -0
  13. package/dist/src/primitives/createTag.jsx +20 -0
  14. package/dist/src/primitives/createTag.jsx.map +1 -0
  15. package/dist/src/primitives/index.d.ts +2 -0
  16. package/dist/src/primitives/index.js +2 -0
  17. package/dist/src/primitives/index.js.map +1 -1
  18. package/dist/src/primitives/useMouse.d.ts +18 -1
  19. package/dist/src/primitives/useMouse.js +142 -59
  20. package/dist/src/primitives/useMouse.js.map +1 -1
  21. package/dist/src/primitives/utils/createBlurredImage.d.ts +56 -0
  22. package/dist/src/primitives/utils/createBlurredImage.js +223 -0
  23. package/dist/src/primitives/utils/createBlurredImage.js.map +1 -0
  24. package/dist/src/primitives/utils/handleNavigation.js +7 -13
  25. package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +8 -5
  28. package/src/primitives/Lazy.tsx +15 -3
  29. package/src/primitives/Virtual.tsx +8 -1
  30. package/src/primitives/announcer/announcer.ts +10 -3
  31. package/src/primitives/announcer/speech.ts +113 -6
  32. package/src/primitives/createTag.tsx +31 -0
  33. package/src/primitives/index.ts +2 -0
  34. package/src/primitives/useMouse.ts +253 -81
  35. package/src/primitives/utils/createBlurredImage.ts +366 -0
  36. package/src/primitives/utils/handleNavigation.ts +9 -14
@@ -12,6 +12,11 @@ export interface SeriesResult {
12
12
  cancel: () => void;
13
13
  }
14
14
 
15
+ // Aria label
16
+ type AriaLabel = { text: string; lang: string };
17
+ const ARIA_PARENT_ID = 'aria-parent';
18
+ let ariaLabelPhrases: AriaLabel[] = [];
19
+
15
20
  /* global SpeechSynthesisErrorEvent */
16
21
  function flattenStrings(series: SpeechType[] = []): SpeechType[] {
17
22
  const flattenedSeries = [];
@@ -40,6 +45,82 @@ function delay(pause: number) {
40
45
  });
41
46
  }
42
47
 
48
+ /**
49
+ * @description This function is called at the end of the speak series
50
+ * @param Phrase is an object containing the text and the language
51
+ */
52
+ function addChildrenToAriaDiv(phrase: AriaLabel) {
53
+ if (phrase?.text?.trim().length === 0) return;
54
+ ariaLabelPhrases.push(phrase);
55
+ }
56
+
57
+ /**
58
+ * @description This function is triggered finally when the speak series is finished and we are to speak the aria labels
59
+ */
60
+ function focusElementForAria() {
61
+ const element = createAriaElement();
62
+
63
+ if (!element) {
64
+ console.error(`ARIA div not found: ${ARIA_PARENT_ID}`);
65
+ return;
66
+ }
67
+
68
+ for (const object of ariaLabelPhrases) {
69
+ const span = document.createElement('span');
70
+
71
+ // TODO: Not sure LG or Samsung support lang attribute on span or switching language
72
+ span.setAttribute('lang', object.lang);
73
+ span.setAttribute('aria-label', object.text);
74
+ element.appendChild(span);
75
+ }
76
+
77
+ // Cleanup
78
+ setTimeout(() => {
79
+ ariaLabelPhrases = [];
80
+ cleanAriaLabelParent();
81
+ focusCanvas();
82
+ }, 100);
83
+ }
84
+
85
+ /**
86
+ * @description Clean the aria label parent after speaking
87
+ */
88
+ function cleanAriaLabelParent(): void {
89
+ const parentTag = document.getElementById(ARIA_PARENT_ID);
90
+ if (parentTag) {
91
+ while (parentTag.firstChild) {
92
+ parentTag.removeChild(parentTag.firstChild);
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * @description Focus the canvas element
99
+ */
100
+ function focusCanvas(): void {
101
+ const canvas = document.getElementById('app')?.firstChild as HTMLElement;
102
+ canvas?.focus();
103
+ }
104
+
105
+ /**
106
+ * @description Create the aria element in the DOM if it doesn't exist
107
+ * @private For xbox, we may need to create a different element each time we wanna use aria
108
+ */
109
+ function createAriaElement(): HTMLDivElement | HTMLElement {
110
+ const aria_container = document.getElementById(ARIA_PARENT_ID);
111
+
112
+ if (!aria_container) {
113
+ const element = document.createElement('div');
114
+ element.setAttribute('id', ARIA_PARENT_ID);
115
+ element.setAttribute('aria-live', 'assertive');
116
+ element.setAttribute('tabindex', '0');
117
+ document.body.appendChild(element);
118
+ return element;
119
+ }
120
+
121
+ return aria_container;
122
+ }
123
+
43
124
  /**
44
125
  * Speak a string
45
126
  *
@@ -82,6 +163,7 @@ function speak(
82
163
 
83
164
  function speakSeries(
84
165
  series: SpeechType,
166
+ aria: boolean,
85
167
  lang: string,
86
168
  voice?: string,
87
169
  root = true,
@@ -118,7 +200,8 @@ function speakSeries(
118
200
 
119
201
  while (active && retriesLeft > 0) {
120
202
  try {
121
- await speak(phrase, utterances, lang, voice);
203
+ if (aria) addChildrenToAriaDiv({ text: phrase, lang });
204
+ else await speak(phrase, utterances, lang, voice);
122
205
  retriesLeft = 0; // Exit retry loop on success
123
206
  } catch (e) {
124
207
  if (e instanceof SpeechSynthesisErrorEvent) {
@@ -152,8 +235,12 @@ function speakSeries(
152
235
 
153
236
  while (active && retriesLeft > 0) {
154
237
  try {
155
- await speak(text, utterances, objectLang, objectVoice?.name);
156
- retriesLeft = 0; // Exit retry loop on success
238
+ if (text) {
239
+ if (aria) addChildrenToAriaDiv({ text, lang: objectLang });
240
+ else
241
+ await speak(text, utterances, objectLang, objectVoice?.name);
242
+ retriesLeft = 0; // Exit retry loop on success
243
+ }
157
244
  } catch (e) {
158
245
  if (e instanceof SpeechSynthesisErrorEvent) {
159
246
  if (e.error === 'network') {
@@ -178,18 +265,22 @@ function speakSeries(
178
265
  }
179
266
  } else if (typeof phrase === 'function') {
180
267
  // Handle functions
181
- const seriesResult = speakSeries(phrase(), lang, voice, false);
268
+ const seriesResult = speakSeries(phrase(), aria, lang, voice, false);
182
269
  nestedSeriesResults.push(seriesResult);
183
270
  await seriesResult.series;
184
271
  } else if (Array.isArray(phrase)) {
185
272
  // Handle nested arrays
186
- const seriesResult = speakSeries(phrase, lang, voice, false);
273
+ const seriesResult = speakSeries(phrase, aria, lang, voice, false);
187
274
  nestedSeriesResults.push(seriesResult);
188
275
  await seriesResult.series;
189
276
  }
190
277
  }
191
278
  } finally {
192
279
  active = false;
280
+ // Call completion logic only for the original (root) series
281
+ if (root && aria) {
282
+ focusElementForAria();
283
+ }
193
284
  }
194
285
  })();
195
286
 
@@ -205,7 +296,21 @@ function speakSeries(
205
296
  if (!active) {
206
297
  return;
207
298
  }
299
+
208
300
  if (root) {
301
+ if (aria) {
302
+ const element = createAriaElement();
303
+
304
+ if (element) {
305
+ ariaLabelPhrases = [];
306
+ cleanAriaLabelParent();
307
+ element.focus();
308
+ focusCanvas();
309
+ }
310
+
311
+ return;
312
+ }
313
+
209
314
  synth.cancel(); // Cancel all ongoing speech
210
315
  }
211
316
  nestedSeriesResults.forEach((nestedSeriesResult) => {
@@ -215,13 +320,15 @@ function speakSeries(
215
320
  },
216
321
  };
217
322
  }
323
+
218
324
  let currentSeries: SeriesResult | undefined;
219
325
  export default function (
220
326
  toSpeak: SpeechType,
327
+ aria: boolean,
221
328
  lang: string = 'en-US',
222
329
  voice?: string,
223
330
  ) {
224
331
  currentSeries && currentSeries.cancel();
225
- currentSeries = speakSeries(toSpeak, lang, voice);
332
+ currentSeries = speakSeries(toSpeak, aria, lang, voice);
226
333
  return currentSeries;
227
334
  }
@@ -0,0 +1,31 @@
1
+ import * as s from 'solid-js'
2
+ import * as lng from '@lightningtv/solid'
3
+
4
+ interface Destroyable {
5
+ (props: lng.NodeProps): s.JSX.Element;
6
+ destroy: () => void;
7
+ }
8
+
9
+ export function createTag(children: s.JSX.Element): Destroyable {
10
+ const [texture, setTexture] = s.createSignal<lng.Texture | null | undefined>(null);
11
+ const Tag = <view
12
+ display='flex'
13
+ onLayout={(n) => {
14
+ if (n.preFlexwidth && n.width !== n.preFlexwidth) {
15
+ n.rtt = true;
16
+ setTimeout(() => setTexture(n.texture), 1);
17
+ }
18
+ }}
19
+ parent={lng.rootNode} children={children}
20
+ textureOptions={{
21
+ preventCleanup: true
22
+ }} /> as any as lng.ElementNode
23
+ Tag.render(false);
24
+
25
+ const TagComponent = (props: lng.NodeProps) => {
26
+ return <view color={0xffffffff} autosize {...props} texture={texture()} />;
27
+ };
28
+ TagComponent.destroy = () => Tag.destroy();
29
+
30
+ return TagComponent;
31
+ }
@@ -21,6 +21,7 @@ export * from './KeepAlive.jsx';
21
21
  export * from './VirtualGrid.jsx';
22
22
  export * from './Virtual.jsx';
23
23
  export * from './utils/withScrolling.js';
24
+ export * from './createTag.jsx';
24
25
  export {
25
26
  type AnyFunction,
26
27
  chainFunctions,
@@ -28,6 +29,7 @@ export {
28
29
  } from './utils/chainFunctions.js';
29
30
  export * from './utils/handleNavigation.js';
30
31
  export { createSpriteMap, type SpriteDef } from './utils/createSpriteMap.js';
32
+ export { createBlurredImage } from './utils/createBlurredImage.js';
31
33
 
32
34
  export type * from './types.js';
33
35
  export type { KeyHandler } from '@lightningtv/core/focusManager';
@@ -1,17 +1,31 @@
1
1
  import type { ElementText, TextNode } from '@lightningtv/core';
2
2
  import {
3
+ Config,
3
4
  ElementNode,
4
5
  activeElement,
5
6
  isElementNode,
7
+ isFunc,
6
8
  isTextNode,
7
9
  rootNode,
8
- Config,
9
- isFunc,
10
10
  } from '@lightningtv/solid';
11
11
  import { makeEventListener } from '@solid-primitives/event-listener';
12
12
  import { useMousePosition } from '@solid-primitives/mouse';
13
13
  import { createScheduled, throttle } from '@solid-primitives/scheduled';
14
- import { createEffect } from 'solid-js';
14
+ import { createEffect, getOwner, runWithOwner } from 'solid-js';
15
+
16
+ type CustomState = `$${string}`;
17
+
18
+ type RenderableNode = ElementNode | ElementText | TextNode;
19
+
20
+ interface MouseStateOptions {
21
+ hoverState: CustomState;
22
+ pressedState: CustomState;
23
+ pressedStateDuration?: number;
24
+ }
25
+
26
+ type UseMouseOptions =
27
+ | { customStates: MouseStateOptions }
28
+ | { customStates: undefined };
15
29
 
16
30
  declare module '@lightningtv/core' {
17
31
  interface ElementNode {
@@ -24,6 +38,29 @@ declare module '@lightningtv/core' {
24
38
  }
25
39
  }
26
40
 
41
+ const DEFAULT_PRESSED_STATE_DURATION = 150;
42
+
43
+ export function addCustomStateToElement(
44
+ element: RenderableNode,
45
+ state: CustomState,
46
+ ): void {
47
+ element.states?.add(state);
48
+ }
49
+
50
+ export function removeCustomStateFromElement(
51
+ element: RenderableNode,
52
+ state: CustomState,
53
+ ): void {
54
+ element?.states?.remove(state);
55
+ }
56
+
57
+ export function hasCustomState(
58
+ element: RenderableNode,
59
+ state: CustomState,
60
+ ): boolean {
61
+ return element.states?.has(state);
62
+ }
63
+
27
64
  function createKeyboardEvent(
28
65
  key: string,
29
66
  keyCode: number,
@@ -41,7 +78,7 @@ function createKeyboardEvent(
41
78
  });
42
79
  }
43
80
 
44
- let scrollTimeout: number;
81
+ let scrollTimeout: ReturnType<typeof setTimeout>;
45
82
  const handleScroll = throttle((e: WheelEvent): void => {
46
83
  const deltaY = e.deltaY;
47
84
  if (deltaY < 0) {
@@ -59,9 +96,38 @@ const handleScroll = throttle((e: WheelEvent): void => {
59
96
  }, 250);
60
97
  }, 250);
61
98
 
62
- const handleClick = (e: MouseEvent): void => {
99
+ function findElementWithCustomState<TApp extends ElementNode>(
100
+ myApp: TApp,
101
+ x: number,
102
+ y: number,
103
+ customState: CustomState,
104
+ ): ElementNode | undefined {
105
+ const result = getChildrenByPosition(myApp, x, y).filter((el) =>
106
+ hasCustomState(el, customState),
107
+ );
108
+
109
+ if (result.length === 0) {
110
+ return undefined;
111
+ }
112
+
113
+ let element: ElementNode | undefined = result[result.length - 1];
114
+
115
+ while (element) {
116
+ const elmParent = element.parent;
117
+ if (elmParent?.forwardStates && hasCustomState(elmParent, customState)) {
118
+ element = elmParent;
119
+ } else {
120
+ break;
121
+ }
122
+ }
123
+
124
+ return element;
125
+ }
126
+
127
+ function findElementByActiveElement(e: MouseEvent): ElementNode | null {
63
128
  const active = activeElement();
64
129
  const precision = Config.rendererOptions?.deviceLogicalPixelRatio || 1;
130
+
65
131
  if (
66
132
  active instanceof ElementNode &&
67
133
  testCollision(
@@ -73,38 +139,95 @@ const handleClick = (e: MouseEvent): void => {
73
139
  (active.height || 0) * precision,
74
140
  )
75
141
  ) {
76
- if (isFunc(active.onMouseClick)) {
77
- active.onMouseClick.call(active, e, active);
78
- return;
142
+ return active;
143
+ }
144
+
145
+ let parent = active?.parent;
146
+ while (parent) {
147
+ if (
148
+ isFunc(parent.onMouseClick) &&
149
+ active &&
150
+ testCollision(
151
+ e.clientX,
152
+ e.clientY,
153
+ ((parent.lng.absX as number) || 0) * precision,
154
+ ((parent.lng.absY as number) || 0) * precision,
155
+ (parent.width || 0) * precision,
156
+ (parent.height || 0) * precision,
157
+ )
158
+ ) {
159
+ return parent;
79
160
  }
161
+ parent = parent.parent;
162
+ }
163
+
164
+ return null;
165
+ }
166
+
167
+ function applyPressedState(
168
+ element: ElementNode,
169
+ pressedState: CustomState,
170
+ pressedStateDuration: number = DEFAULT_PRESSED_STATE_DURATION,
171
+ ): void {
172
+ addCustomStateToElement(element, pressedState);
173
+ setTimeout(() => {
174
+ removeCustomStateFromElement(element, pressedState);
175
+ }, pressedStateDuration);
176
+ }
177
+
178
+ function handleElementClick(
179
+ clickedElement: ElementNode,
180
+ e: MouseEvent,
181
+ customStates?: MouseStateOptions,
182
+ ): void {
183
+ if (customStates?.pressedState) {
184
+ applyPressedState(
185
+ clickedElement,
186
+ customStates.pressedState,
187
+ customStates.pressedStateDuration,
188
+ );
189
+ }
190
+
191
+ if (isFunc(clickedElement.onMouseClick)) {
192
+ clickedElement.onMouseClick(e, clickedElement);
193
+ return;
194
+ } else if (isFunc(clickedElement.onEnter)) {
195
+ clickedElement.onEnter();
196
+ return;
197
+ }
80
198
 
199
+ clickedElement.setFocus();
200
+ setTimeout(() => {
81
201
  document.dispatchEvent(createKeyboardEvent('Enter', 13));
82
202
  setTimeout(
83
203
  () =>
84
204
  document.body.dispatchEvent(createKeyboardEvent('Enter', 13, 'keyup')),
85
205
  1,
86
206
  );
87
- } else {
88
- let parent = active?.parent;
89
- while (parent) {
90
- if (
91
- isFunc(parent.onMouseClick) &&
92
- testCollision(
207
+ }, 1);
208
+ }
209
+
210
+ function createHandleClick<TApp extends ElementNode>(
211
+ myApp: TApp,
212
+ customStates?: MouseStateOptions,
213
+ ) {
214
+ return (e: MouseEvent): void => {
215
+ const clickedElement = customStates
216
+ ? findElementWithCustomState(
217
+ myApp,
93
218
  e.clientX,
94
219
  e.clientY,
95
- ((parent.lng.absX as number) || 0) * precision,
96
- ((parent.lng.absY as number) || 0) * precision,
97
- (parent.width || 0) * precision,
98
- (parent.height || 0) * precision,
220
+ customStates.hoverState,
99
221
  )
100
- ) {
101
- parent.onMouseClick.call(parent, e, active!);
102
- return;
103
- }
104
- parent = parent.parent;
222
+ : findElementByActiveElement(e);
223
+
224
+ if (!clickedElement) {
225
+ return;
105
226
  }
106
- }
107
- };
227
+
228
+ handleElementClick(clickedElement, e, customStates);
229
+ };
230
+ }
108
231
 
109
232
  function testCollision(
110
233
  px: number,
@@ -117,106 +240,155 @@ function testCollision(
117
240
  return px >= cx && px <= cx + cw && py >= cy && py <= cy + ch;
118
241
  }
119
242
 
120
- function getChildrenByPosition(
121
- node: ElementNode,
243
+ function isNodeAtPosition(
244
+ node: ElementNode | ElementText | TextNode,
122
245
  x: number,
123
246
  y: number,
124
- ): ElementNode[] {
125
- const result: ElementNode[] = [];
126
- const precision = Config.rendererOptions?.deviceLogicalPixelRatio || 1;
247
+ precision: number,
248
+ ): node is ElementNode {
249
+ if (!isElementNode(node)) {
250
+ return false;
251
+ }
252
+
253
+ return (
254
+ node.alpha !== 0 &&
255
+ !node.skipFocus &&
256
+ testCollision(
257
+ x,
258
+ y,
259
+ ((node.lng.absX as number) || 0) * precision,
260
+ ((node.lng.absY as number) || 0) * precision,
261
+ (node.width || 0) * precision,
262
+ (node.height || 0) * precision,
263
+ )
264
+ );
265
+ }
266
+
267
+ function findHighestZIndexNode(nodes: ElementNode[]): ElementNode | undefined {
268
+ if (nodes.length === 0) {
269
+ return undefined;
270
+ }
271
+
272
+ if (nodes.length === 1) {
273
+ return nodes[0];
274
+ }
275
+
276
+ let maxZIndex = -1;
277
+ let highestNode: ElementNode | undefined = undefined;
278
+
279
+ for (const node of nodes) {
280
+ const zIndex = node.zIndex ?? -1;
281
+ if (zIndex >= maxZIndex) {
282
+ maxZIndex = zIndex;
283
+ highestNode = node;
284
+ }
285
+ }
127
286
 
287
+ return highestNode;
288
+ }
289
+
290
+ function getChildrenByPosition<TElement extends ElementNode = ElementNode>(
291
+ node: TElement,
292
+ x: number,
293
+ y: number,
294
+ ): TElement[] {
295
+ const result: TElement[] = [];
296
+ const precision = Config.rendererOptions?.deviceLogicalPixelRatio || 1;
128
297
  // Queue for BFS
298
+
129
299
  let queue: (ElementNode | ElementText | TextNode)[] = [node];
130
300
 
131
301
  while (queue.length > 0) {
132
302
  // Process nodes at the current level
133
- const currentLevelNodes: ElementNode[] = [];
134
-
135
- for (const currentNode of queue) {
136
- if (
137
- isElementNode(currentNode) &&
138
- currentNode.alpha !== 0 &&
139
- !currentNode.skipFocus &&
140
- testCollision(
141
- x,
142
- y,
143
- ((currentNode.lng.absX as number) || 0) * precision,
144
- ((currentNode.lng.absY as number) || 0) * precision,
145
- (currentNode.width || 0) * precision,
146
- (currentNode.height || 0) * precision,
147
- )
148
- ) {
149
- currentLevelNodes.push(currentNode);
150
- }
151
- }
303
+ const currentLevelNodes = queue.filter((currentNode) =>
304
+ isNodeAtPosition(currentNode, x, y, precision),
305
+ );
152
306
 
153
- const size = currentLevelNodes.length;
154
- if (size === 0) {
307
+ if (currentLevelNodes.length === 0) {
155
308
  break;
156
309
  }
157
310
 
158
- let highestZIndexNode = null;
159
- if (size === 1) {
160
- highestZIndexNode = currentLevelNodes[0];
161
- } else {
162
- let maxZIndex = -1;
163
-
164
- for (const node of currentLevelNodes) {
165
- const zIndex = node.zIndex ?? -1;
166
- if (zIndex > maxZIndex) {
167
- maxZIndex = zIndex;
168
- highestZIndexNode = node;
169
- } else if (zIndex === maxZIndex) {
170
- highestZIndexNode = node;
171
- }
172
- }
173
- }
311
+ const highestZIndexNode = findHighestZIndexNode(currentLevelNodes);
174
312
 
175
- if (highestZIndexNode && !isTextNode(highestZIndexNode)) {
176
- result.push(highestZIndexNode);
177
- queue = highestZIndexNode.children;
178
- } else {
179
- queue = [];
313
+ if (!highestZIndexNode || isTextNode(highestZIndexNode)) {
314
+ break;
180
315
  }
316
+
317
+ result.push(highestZIndexNode as TElement);
318
+ queue = highestZIndexNode.children;
181
319
  }
182
320
 
183
321
  return result;
184
322
  }
185
323
 
186
- export function useMouse(
187
- myApp: ElementNode = rootNode,
324
+ export function useMouse<TApp extends ElementNode = ElementNode>(
325
+ myApp: TApp = rootNode as TApp,
188
326
  throttleBy: number = 100,
327
+ options?: UseMouseOptions,
189
328
  ): void {
190
329
  const pos = useMousePosition();
191
330
  const scheduled = createScheduled((fn) => throttle(fn, throttleBy));
331
+ let previousElement: ElementNode | null = null;
332
+ const customStates = options?.customStates;
333
+ const hoverState = customStates?.hoverState;
334
+ const handleClick = createHandleClick(myApp, customStates);
335
+ const owner = getOwner();
336
+ const handleClickContext = (e: MouseEvent) => {
337
+ runWithOwner(owner, () => handleClick(e));
338
+ };
339
+
192
340
  makeEventListener(window, 'wheel', handleScroll);
193
- makeEventListener(window, 'click', handleClick);
341
+ makeEventListener(window, 'click', handleClickContext);
194
342
  createEffect(() => {
195
343
  if (scheduled()) {
196
344
  const result = getChildrenByPosition(myApp, pos.x, pos.y).filter(
197
- (el) => el.focus || el.onFocus || el.onEnter,
345
+ (el) =>
346
+ !!(
347
+ el.onEnter ||
348
+ el.onMouseClick ||
349
+ el.onFocus ||
350
+ el[Config.focusStateKey] ||
351
+ (hoverState ? el[hoverState] : false)
352
+ ),
198
353
  );
199
354
 
200
355
  if (result.length) {
201
- let activeElm = result[result.length - 1];
356
+ let activeElm: ElementNode | undefined = result[result.length - 1];
202
357
 
203
358
  while (activeElm) {
204
359
  const elmParent = activeElm.parent;
205
360
  if (elmParent?.forwardStates) {
206
- activeElm = activeElm.parent;
361
+ activeElm = elmParent;
207
362
  } else {
208
363
  break;
209
364
  }
210
365
  }
211
366
 
367
+ if (!activeElm) {
368
+ return;
369
+ }
370
+
212
371
  // Update Row & Column Selected property
213
- const activeElmParent = activeElm?.parent;
214
- if (activeElm && activeElmParent?.selected !== undefined) {
372
+ const activeElmParent = activeElm.parent;
373
+ if (activeElmParent?.selected !== undefined) {
215
374
  activeElmParent.selected =
216
375
  activeElmParent.children.indexOf(activeElm);
217
376
  }
218
377
 
219
- activeElm?.setFocus();
378
+ if (previousElement && previousElement !== activeElm && hoverState) {
379
+ removeCustomStateFromElement(previousElement, hoverState);
380
+ }
381
+
382
+ if (hoverState) {
383
+ addCustomStateToElement(activeElm, hoverState);
384
+ } else {
385
+ activeElm.setFocus();
386
+ }
387
+
388
+ previousElement = activeElm;
389
+ } else if (previousElement && hoverState) {
390
+ removeCustomStateFromElement(previousElement, hoverState);
391
+ previousElement = null;
220
392
  }
221
393
  }
222
394
  });