@lightningtv/solid 3.0.0-2 → 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 (126) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +6 -0
  3. package/dist/src/jsx-runtime.d.ts +1 -3
  4. package/dist/src/primitives/Column.jsx +9 -10
  5. package/dist/src/primitives/Column.jsx.map +1 -1
  6. package/dist/src/primitives/Grid.d.ts +15 -6
  7. package/dist/src/primitives/Grid.jsx +35 -22
  8. package/dist/src/primitives/Grid.jsx.map +1 -1
  9. package/dist/src/primitives/Image.d.ts +8 -0
  10. package/dist/src/primitives/Image.jsx +24 -0
  11. package/dist/src/primitives/Image.jsx.map +1 -0
  12. package/dist/src/primitives/KeepAlive.d.ts +30 -0
  13. package/dist/src/primitives/KeepAlive.jsx +77 -0
  14. package/dist/src/primitives/KeepAlive.jsx.map +1 -0
  15. package/dist/src/primitives/Lazy.d.ts +8 -7
  16. package/dist/src/primitives/Lazy.jsx +49 -23
  17. package/dist/src/primitives/Lazy.jsx.map +1 -1
  18. package/dist/src/primitives/Marquee.d.ts +64 -0
  19. package/dist/src/primitives/Marquee.jsx +86 -0
  20. package/dist/src/primitives/Marquee.jsx.map +1 -0
  21. package/dist/src/primitives/Preserve.d.ts +4 -0
  22. package/dist/src/primitives/Preserve.jsx +11 -0
  23. package/dist/src/primitives/Preserve.jsx.map +1 -0
  24. package/dist/src/primitives/Row.jsx +9 -10
  25. package/dist/src/primitives/Row.jsx.map +1 -1
  26. package/dist/src/primitives/Suspense.d.ts +22 -0
  27. package/dist/src/primitives/Suspense.jsx +33 -0
  28. package/dist/src/primitives/Suspense.jsx.map +1 -0
  29. package/dist/src/primitives/Virtual.d.ts +18 -0
  30. package/dist/src/primitives/Virtual.jsx +434 -0
  31. package/dist/src/primitives/Virtual.jsx.map +1 -0
  32. package/dist/src/primitives/VirtualGrid.d.ts +13 -0
  33. package/dist/src/primitives/VirtualGrid.jsx +139 -0
  34. package/dist/src/primitives/VirtualGrid.jsx.map +1 -0
  35. package/dist/src/primitives/VirtualList.d.ts +11 -0
  36. package/dist/src/primitives/VirtualList.jsx +96 -0
  37. package/dist/src/primitives/VirtualList.jsx.map +1 -0
  38. package/dist/src/primitives/VirtualRow.d.ts +13 -0
  39. package/dist/src/primitives/VirtualRow.jsx +97 -0
  40. package/dist/src/primitives/VirtualRow.jsx.map +1 -0
  41. package/dist/src/primitives/Visible.d.ts +0 -1
  42. package/dist/src/primitives/Visible.jsx +1 -1
  43. package/dist/src/primitives/Visible.jsx.map +1 -1
  44. package/dist/src/primitives/announcer/announcer.d.ts +2 -0
  45. package/dist/src/primitives/announcer/announcer.js +7 -5
  46. package/dist/src/primitives/announcer/announcer.js.map +1 -1
  47. package/dist/src/primitives/announcer/index.d.ts +5 -1
  48. package/dist/src/primitives/announcer/index.js +8 -2
  49. package/dist/src/primitives/announcer/index.js.map +1 -1
  50. package/dist/src/primitives/announcer/speech.d.ts +2 -2
  51. package/dist/src/primitives/announcer/speech.js +157 -28
  52. package/dist/src/primitives/announcer/speech.js.map +1 -1
  53. package/dist/src/primitives/createFocusStack.d.ts +4 -4
  54. package/dist/src/primitives/createFocusStack.jsx +15 -6
  55. package/dist/src/primitives/createFocusStack.jsx.map +1 -1
  56. package/dist/src/primitives/createTag.d.ts +8 -0
  57. package/dist/src/primitives/createTag.jsx +20 -0
  58. package/dist/src/primitives/createTag.jsx.map +1 -0
  59. package/dist/src/primitives/index.d.ts +13 -3
  60. package/dist/src/primitives/index.js +13 -3
  61. package/dist/src/primitives/index.js.map +1 -1
  62. package/dist/src/primitives/types.d.ts +3 -0
  63. package/dist/src/primitives/useHold.d.ts +27 -0
  64. package/dist/src/primitives/useHold.js +54 -0
  65. package/dist/src/primitives/useHold.js.map +1 -0
  66. package/dist/src/primitives/useMouse.d.ts +24 -1
  67. package/dist/src/primitives/useMouse.js +153 -47
  68. package/dist/src/primitives/useMouse.js.map +1 -1
  69. package/dist/src/primitives/utils/chainFunctions.d.ts +30 -4
  70. package/dist/src/primitives/utils/chainFunctions.js +14 -3
  71. package/dist/src/primitives/utils/chainFunctions.js.map +1 -1
  72. package/dist/src/primitives/utils/createBlurredImage.d.ts +56 -0
  73. package/dist/src/primitives/utils/createBlurredImage.js +223 -0
  74. package/dist/src/primitives/utils/createBlurredImage.js.map +1 -0
  75. package/dist/src/primitives/utils/createSpriteMap.d.ts +2 -2
  76. package/dist/src/primitives/utils/createSpriteMap.js.map +1 -1
  77. package/dist/src/primitives/utils/handleNavigation.d.ts +85 -5
  78. package/dist/src/primitives/utils/handleNavigation.js +242 -69
  79. package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
  80. package/dist/src/primitives/utils/withScrolling.d.ts +8 -1
  81. package/dist/src/primitives/utils/withScrolling.js +25 -6
  82. package/dist/src/primitives/utils/withScrolling.js.map +1 -1
  83. package/dist/src/render.d.ts +6 -5
  84. package/dist/src/render.js +4 -0
  85. package/dist/src/render.js.map +1 -1
  86. package/dist/src/solidOpts.d.ts +3 -2
  87. package/dist/src/solidOpts.js +31 -15
  88. package/dist/src/solidOpts.js.map +1 -1
  89. package/dist/src/universal.d.ts +25 -0
  90. package/dist/src/universal.js +232 -0
  91. package/dist/src/universal.js.map +1 -0
  92. package/dist/src/utils.d.ts +2 -0
  93. package/dist/src/utils.js +8 -0
  94. package/dist/src/utils.js.map +1 -1
  95. package/dist/tsconfig.tsbuildinfo +1 -1
  96. package/jsx-runtime.d.ts +2 -4
  97. package/package.json +19 -10
  98. package/src/primitives/Column.tsx +10 -12
  99. package/src/primitives/Grid.tsx +57 -33
  100. package/src/primitives/Image.tsx +36 -0
  101. package/src/primitives/KeepAlive.tsx +124 -0
  102. package/src/primitives/Lazy.tsx +60 -37
  103. package/src/primitives/Marquee.tsx +149 -0
  104. package/src/primitives/Preserve.tsx +18 -0
  105. package/src/primitives/Row.tsx +11 -12
  106. package/src/primitives/Suspense.tsx +39 -0
  107. package/src/primitives/Virtual.tsx +478 -0
  108. package/src/primitives/VirtualGrid.tsx +199 -0
  109. package/src/primitives/Visible.tsx +1 -2
  110. package/src/primitives/announcer/announcer.ts +16 -10
  111. package/src/primitives/announcer/index.ts +12 -2
  112. package/src/primitives/announcer/speech.ts +188 -27
  113. package/src/primitives/createFocusStack.tsx +18 -7
  114. package/src/primitives/createTag.tsx +31 -0
  115. package/src/primitives/index.ts +17 -3
  116. package/src/primitives/types.ts +10 -0
  117. package/src/primitives/useHold.ts +69 -0
  118. package/src/primitives/useMouse.ts +283 -66
  119. package/src/primitives/utils/chainFunctions.ts +40 -9
  120. package/src/primitives/utils/createBlurredImage.ts +366 -0
  121. package/src/primitives/utils/createSpriteMap.ts +6 -4
  122. package/src/primitives/utils/handleNavigation.ts +307 -84
  123. package/src/primitives/utils/withScrolling.ts +47 -16
  124. package/src/render.ts +9 -7
  125. package/src/solidOpts.ts +34 -19
  126. package/src/utils.ts +10 -0
@@ -4,6 +4,7 @@ export * from './createInfiniteItems.js';
4
4
  export * from './useMouse.js';
5
5
  export * from './portal.jsx';
6
6
  export * from './Lazy.jsx';
7
+ export * from './Image.jsx';
7
8
  export * from './Visible.jsx';
8
9
  export * from './router.js';
9
10
  export * from './Column.jsx';
@@ -11,11 +12,24 @@ export * from './Row.jsx';
11
12
  export * from './Grid.jsx';
12
13
  export * from './FPSCounter.jsx';
13
14
  export * from './FadeInOut.jsx';
15
+ export * from './Preserve.jsx';
16
+ export * from './Suspense.jsx';
17
+ export * from './Marquee.jsx';
14
18
  export * from './createFocusStack.jsx';
15
- export { withScrolling } from './utils/withScrolling.js';
16
- export { chainFunctions } from './utils/chainFunctions.js';
17
- export { handleNavigation, onGridFocus } from './utils/handleNavigation.js';
19
+ export * from './useHold.js';
20
+ export * from './KeepAlive.jsx';
21
+ export * from './VirtualGrid.jsx';
22
+ export * from './Virtual.jsx';
23
+ export * from './utils/withScrolling.js';
24
+ export * from './createTag.jsx';
25
+ export {
26
+ type AnyFunction,
27
+ chainFunctions,
28
+ chainRefs,
29
+ } from './utils/chainFunctions.js';
30
+ export * from './utils/handleNavigation.js';
18
31
  export { createSpriteMap, type SpriteDef } from './utils/createSpriteMap.js';
32
+ export { createBlurredImage } from './utils/createBlurredImage.js';
19
33
 
20
34
  export type * from './types.js';
21
35
  export type { KeyHandler } from '@lightningtv/core/focusManager';
@@ -1,5 +1,6 @@
1
1
  import type { ElementNode, NodeProps, NodeStyles } from '@lightningtv/solid';
2
2
  import type { KeyHandler } from '@lightningtv/core/focusManager';
3
+
3
4
  export type OnSelectedChanged = (
4
5
  this: NavigableElement,
5
6
  selectedIndex: number,
@@ -7,6 +8,7 @@ export type OnSelectedChanged = (
7
8
  active: ElementNode,
8
9
  lastSelectedIndex?: number,
9
10
  ) => void;
11
+
10
12
  export interface NavigableProps extends NodeProps {
11
13
  /** function to be called when the selected of the component changes */
12
14
  onSelectedChanged?: OnSelectedChanged;
@@ -40,11 +42,19 @@ export interface NavigableProps extends NodeProps {
40
42
  * Wrap the row so active goes back to the beginning of the row
41
43
  */
42
44
  wrap?: boolean;
45
+
46
+ /** function to be called when scrolled */
47
+ onScrolled?: (
48
+ elm: NavigableElement,
49
+ offset: number,
50
+ isInitial: boolean,
51
+ ) => void;
43
52
  }
44
53
 
45
54
  // @ts-expect-error animationSettings is not identical - weird
46
55
  export interface NavigableElement extends ElementNode, NavigableProps {
47
56
  selected: number;
57
+ scrollToIndex: (this: NavigableElement, index: number) => void;
48
58
  }
49
59
 
50
60
  export interface NavigableStyleProperties {
@@ -0,0 +1,69 @@
1
+ import { createMemo } from 'solid-js';
2
+
3
+ export type UseHoldProps = {
4
+ onHold: () => void;
5
+ onEnter: () => void;
6
+ onRelease?: () => void;
7
+ holdThreshold?: number;
8
+ performOnEnterImmediately?: boolean;
9
+ };
10
+
11
+ /**
12
+ * @example
13
+ * const [holdRight, releaseRight] = useHold({
14
+ * onHold: handleHoldRight,
15
+ * onEnter: handleOnRight,
16
+ * onRelease: handleReleaseHold,
17
+ * holdThreshold: 200,
18
+ * performOnEnterImmediately: true
19
+ * });
20
+ *
21
+ * <View
22
+ * onRight={holdRight}
23
+ * onRightRelease={releaseRight}
24
+ * />
25
+ *
26
+ * @param {UseHoldProps} props - The properties for configuring the hold behavior.
27
+ * @returns {[() => boolean, () => boolean]} A tuple containing `startHold` and `releaseHold` functions.
28
+ */
29
+
30
+ export function useHold(props: UseHoldProps) {
31
+ const holdThreshold = createMemo(() => props.holdThreshold ?? 500);
32
+ const performOnEnterImmediately = createMemo(
33
+ () => props.performOnEnterImmediately ?? false,
34
+ );
35
+
36
+ let holdTimeout = -1;
37
+ let wasHeld = false;
38
+
39
+ const startHold = () => {
40
+ if (holdTimeout === -1) {
41
+ if (performOnEnterImmediately()) {
42
+ props.onEnter();
43
+ }
44
+ holdTimeout = setTimeout(() => {
45
+ wasHeld = true;
46
+ props.onHold();
47
+ }, holdThreshold()) as unknown as number;
48
+ }
49
+ return true;
50
+ };
51
+
52
+ const releaseHold = () => {
53
+ if (holdTimeout !== -1) {
54
+ clearTimeout(holdTimeout);
55
+ holdTimeout = -1;
56
+ if (!wasHeld) {
57
+ if (!performOnEnterImmediately()) props.onEnter();
58
+ return;
59
+ }
60
+ props.onRelease?.();
61
+ wasHeld = false;
62
+ }
63
+ return true;
64
+ };
65
+
66
+ return [startHold, releaseHold];
67
+ }
68
+
69
+ export default useHold;
@@ -1,16 +1,65 @@
1
- import type { ElementText, INode, TextNode } from '@lightningtv/core';
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
10
  } from '@lightningtv/solid';
10
11
  import { makeEventListener } from '@solid-primitives/event-listener';
11
12
  import { useMousePosition } from '@solid-primitives/mouse';
12
13
  import { createScheduled, throttle } from '@solid-primitives/scheduled';
13
- 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 };
29
+
30
+ declare module '@lightningtv/core' {
31
+ interface ElementNode {
32
+ /** function to be called on mouse click */
33
+ onMouseClick?: (
34
+ this: ElementNode,
35
+ event: MouseEvent,
36
+ active: ElementNode,
37
+ ) => void;
38
+ }
39
+ }
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
+ }
14
63
 
15
64
  function createKeyboardEvent(
16
65
  key: string,
@@ -29,6 +78,7 @@ function createKeyboardEvent(
29
78
  });
30
79
  }
31
80
 
81
+ let scrollTimeout: ReturnType<typeof setTimeout>;
32
82
  const handleScroll = throttle((e: WheelEvent): void => {
33
83
  const deltaY = e.deltaY;
34
84
  if (deltaY < 0) {
@@ -36,30 +86,148 @@ const handleScroll = throttle((e: WheelEvent): void => {
36
86
  } else if (deltaY > 0) {
37
87
  document.body.dispatchEvent(createKeyboardEvent('ArrowDown', 40));
38
88
  }
89
+
90
+ // clear the last timeout if the user is still scrolling
91
+ clearTimeout(scrollTimeout);
92
+ // after 250ms of no scroll events, we send a keyup event to stop the scrolling
93
+ scrollTimeout = setTimeout(() => {
94
+ document.body.dispatchEvent(createKeyboardEvent('ArrowUp', 38, 'keyup'));
95
+ document.body.dispatchEvent(createKeyboardEvent('ArrowDown', 40, 'keyup'));
96
+ }, 250);
39
97
  }, 250);
40
98
 
41
- 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 {
42
128
  const active = activeElement();
43
129
  const precision = Config.rendererOptions?.deviceLogicalPixelRatio || 1;
130
+
44
131
  if (
45
132
  active instanceof ElementNode &&
46
133
  testCollision(
47
134
  e.clientX,
48
135
  e.clientY,
49
- (active.lng.absX as number) || 0 * precision,
50
- (active.lng.absY as number) || 0 * precision,
51
- active.width || 0 * precision,
52
- active.height || 0 * precision,
136
+ ((active.lng.absX as number) || 0) * precision,
137
+ ((active.lng.absY as number) || 0) * precision,
138
+ (active.width || 0) * precision,
139
+ (active.height || 0) * precision,
53
140
  )
54
141
  ) {
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;
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
+ }
198
+
199
+ clickedElement.setFocus();
200
+ setTimeout(() => {
55
201
  document.dispatchEvent(createKeyboardEvent('Enter', 13));
56
202
  setTimeout(
57
203
  () =>
58
204
  document.body.dispatchEvent(createKeyboardEvent('Enter', 13, 'keyup')),
59
205
  1,
60
206
  );
61
- }
62
- };
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,
218
+ e.clientX,
219
+ e.clientY,
220
+ customStates.hoverState,
221
+ )
222
+ : findElementByActiveElement(e);
223
+
224
+ if (!clickedElement) {
225
+ return;
226
+ }
227
+
228
+ handleElementClick(clickedElement, e, customStates);
229
+ };
230
+ }
63
231
 
64
232
  function testCollision(
65
233
  px: number,
@@ -72,106 +240,155 @@ function testCollision(
72
240
  return px >= cx && px <= cx + cw && py >= cy && py <= cy + ch;
73
241
  }
74
242
 
75
- function getChildrenByPosition(
76
- node: ElementNode,
243
+ function isNodeAtPosition(
244
+ node: ElementNode | ElementText | TextNode,
77
245
  x: number,
78
246
  y: number,
79
- ): ElementNode[] {
80
- const result: ElementNode[] = [];
81
- 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
+ }
82
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;
83
297
  // Queue for BFS
298
+
84
299
  let queue: (ElementNode | ElementText | TextNode)[] = [node];
85
300
 
86
301
  while (queue.length > 0) {
87
302
  // Process nodes at the current level
88
- const currentLevelNodes: ElementNode[] = [];
89
-
90
- for (const currentNode of queue) {
91
- if (
92
- isElementNode(currentNode) &&
93
- currentNode.alpha !== 0 &&
94
- !currentNode.skipFocus &&
95
- testCollision(
96
- x,
97
- y,
98
- (currentNode.lng.absX as number) || 0 * precision,
99
- (currentNode.lng.absY as number) || 0 * precision,
100
- (currentNode.width || 0) * precision,
101
- (currentNode.height || 0) * precision,
102
- )
103
- ) {
104
- currentLevelNodes.push(currentNode);
105
- }
106
- }
303
+ const currentLevelNodes = queue.filter((currentNode) =>
304
+ isNodeAtPosition(currentNode, x, y, precision),
305
+ );
107
306
 
108
- const size = currentLevelNodes.length;
109
- if (size === 0) {
307
+ if (currentLevelNodes.length === 0) {
110
308
  break;
111
309
  }
112
310
 
113
- let highestZIndexNode = null;
114
- if (size === 1) {
115
- highestZIndexNode = currentLevelNodes[0];
116
- } else {
117
- let maxZIndex = -1;
118
-
119
- for (const node of currentLevelNodes) {
120
- const zIndex = node.zIndex ?? -1;
121
- if (zIndex > maxZIndex) {
122
- maxZIndex = zIndex;
123
- highestZIndexNode = node;
124
- } else if (zIndex === maxZIndex) {
125
- highestZIndexNode = node;
126
- }
127
- }
128
- }
311
+ const highestZIndexNode = findHighestZIndexNode(currentLevelNodes);
129
312
 
130
- if (highestZIndexNode && !isTextNode(highestZIndexNode)) {
131
- result.push(highestZIndexNode);
132
- queue = highestZIndexNode.children;
133
- } else {
134
- queue = [];
313
+ if (!highestZIndexNode || isTextNode(highestZIndexNode)) {
314
+ break;
135
315
  }
316
+
317
+ result.push(highestZIndexNode as TElement);
318
+ queue = highestZIndexNode.children;
136
319
  }
137
320
 
138
321
  return result;
139
322
  }
140
323
 
141
- export function useMouse(
142
- myApp: ElementNode = rootNode,
324
+ export function useMouse<TApp extends ElementNode = ElementNode>(
325
+ myApp: TApp = rootNode as TApp,
143
326
  throttleBy: number = 100,
327
+ options?: UseMouseOptions,
144
328
  ): void {
145
329
  const pos = useMousePosition();
146
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
+
147
340
  makeEventListener(window, 'wheel', handleScroll);
148
- makeEventListener(window, 'click', handleClick);
341
+ makeEventListener(window, 'click', handleClickContext);
149
342
  createEffect(() => {
150
343
  if (scheduled()) {
151
344
  const result = getChildrenByPosition(myApp, pos.x, pos.y).filter(
152
- (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
+ ),
153
353
  );
154
354
 
155
355
  if (result.length) {
156
- let activeElm = result[result.length - 1];
356
+ let activeElm: ElementNode | undefined = result[result.length - 1];
157
357
 
158
358
  while (activeElm) {
159
359
  const elmParent = activeElm.parent;
160
360
  if (elmParent?.forwardStates) {
161
- activeElm = activeElm.parent;
361
+ activeElm = elmParent;
162
362
  } else {
163
363
  break;
164
364
  }
165
365
  }
166
366
 
367
+ if (!activeElm) {
368
+ return;
369
+ }
370
+
167
371
  // Update Row & Column Selected property
168
- const activeElmParent = activeElm?.parent;
169
- if (activeElm && activeElmParent?.selected !== undefined) {
372
+ const activeElmParent = activeElm.parent;
373
+ if (activeElmParent?.selected !== undefined) {
170
374
  activeElmParent.selected =
171
375
  activeElmParent.children.indexOf(activeElm);
172
376
  }
173
377
 
174
- 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;
175
392
  }
176
393
  }
177
394
  });
@@ -1,13 +1,29 @@
1
- type ChainableFunction = (...args: unknown[]) => unknown;
1
+ import * as s from 'solid-js';
2
2
 
3
- export function chainFunctions(...args: ChainableFunction[]): ChainableFunction;
4
- export function chainFunctions<T>(...args: (ChainableFunction | T)[]): T;
3
+ export type AnyFunction = (this: any, ...args: any[]) => any;
5
4
 
6
- // take an array of functions and if you return true from a function, it will stop the chain
7
- export function chainFunctions<T extends ChainableFunction>(
8
- ...args: (ChainableFunction | T)[]
9
- ) {
10
- const onlyFunctions = args.filter((func) => typeof func === 'function');
5
+ /**
6
+ * take an array of functions and if you return `true` from a function, it will stop the chain
7
+ * @param fns list of functions to chain together, can be `undefined`, `null`, or `false` to skip them
8
+ * @returns a function that will call each function in the list until one returns `true` or all functions are called.
9
+ * If no functions are provided, it will return `undefined`.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * function Button (props: NodeProps) {
14
+ * function onEnter (el: ElementNode) {...}
15
+ * return <view onEnter={chainFunctions(props.onEnter, onEnter)} />
16
+ * }
17
+ * ```
18
+ */
19
+ export function chainFunctions<T extends AnyFunction>(...fns: T[]): T;
20
+ export function chainFunctions<T extends AnyFunction>(
21
+ ...fns: (T | undefined | null | false)[]
22
+ ): T | undefined;
23
+ export function chainFunctions(
24
+ ...fns: (AnyFunction | undefined | null | false)[]
25
+ ): AnyFunction | undefined {
26
+ const onlyFunctions = fns.filter((func) => typeof func === 'function');
11
27
  if (onlyFunctions.length === 0) {
12
28
  return undefined;
13
29
  }
@@ -16,7 +32,7 @@ export function chainFunctions<T extends ChainableFunction>(
16
32
  return onlyFunctions[0];
17
33
  }
18
34
 
19
- return function (this: unknown | T, ...innerArgs: unknown[]) {
35
+ return function (...innerArgs) {
20
36
  let result;
21
37
  for (const func of onlyFunctions) {
22
38
  result = func.apply(this, innerArgs);
@@ -27,3 +43,18 @@ export function chainFunctions<T extends ChainableFunction>(
27
43
  return result;
28
44
  };
29
45
  }
46
+
47
+ /**
48
+ * Utility for chaining multiple `ref` assignments with `props.ref` forwarding.
49
+ * @param refs list of ref setters. Can be a `props.ref` prop for ref forwarding or a setter to a local variable (`el => ref = el`).
50
+ * @example
51
+ * ```tsx
52
+ * function Button (props: NodeProps) {
53
+ * let localRef: ElementNode | undefined
54
+ * return <view ref={chainRefs(props.ref, el => localRef = el)} />
55
+ * }
56
+ * ```
57
+ */
58
+ export const chainRefs = chainFunctions as <T>(
59
+ ...refs: (s.Ref<T> | undefined)[]
60
+ ) => (el: T) => void;