@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
@@ -1,25 +1,51 @@
1
- import { ValidComponent, For, createSignal, createMemo } from "solid-js";
2
- import { Dynamic, type NodeProps, type ElementNode, isFunction } from "@lightningtv/solid";
1
+ import { For, createSignal, createMemo, createEffect, JSX } from "solid-js";
2
+ import { type NodeProps, ElementNode, NewOmit } from "@lightningtv/solid";
3
+ import { chainRefs } from "./utils/chainFunctions.js";
3
4
 
4
- export const Grid = <T,>(props: {
5
- item: ValidComponent;
5
+ export interface GridItemProps<T> {
6
+ item: T
7
+ index: number
8
+ width: number
9
+ height: number
10
+ x: number
11
+ y: number
12
+ }
13
+
14
+ export interface GridProps<T> extends NewOmit<NodeProps, 'children'> {
15
+ items: readonly T[];
16
+ children: (props: GridItemProps<T>) => JSX.Element,
6
17
  itemHeight?: number;
7
18
  itemWidth?: number;
8
19
  itemOffset?: number;
9
- items: T[];
10
20
  columns?: number;
11
21
  looping?: boolean;
12
22
  scroll?: "auto" | "none";
13
23
  onSelectedChanged?: (index: number, grid: ElementNode, elm?: ElementNode) => void;
14
- } & NodeProps) => {
24
+ }
25
+
26
+ export function Grid<T>(props: GridProps<T>): JSX.Element {
27
+
15
28
  const [focusedIndex, setFocusedIndex] = createSignal(0);
16
29
  const baseColumns = 4;
17
30
 
31
+ const itemWidth = () => props.itemWidth ?? 300
32
+ const itemHeight = () => props.itemHeight ?? 300
33
+
18
34
  const columns = createMemo(() => props.columns || baseColumns);
19
- const totalWidth = createMemo(() => (props.itemWidth ?? 300) + (props.itemOffset ?? 0));
20
- const totalHeight = createMemo(() => (props.itemHeight ?? 300) + (props.itemOffset ?? 0));
35
+ const totalWidth = createMemo(() => itemWidth() + (props.itemOffset ?? 0));
36
+ const totalHeight = createMemo(() => itemHeight() + (props.itemOffset ?? 0));
37
+
38
+ function focus() {
39
+ const focusedElm = gridRef.children[focusedIndex()];
40
+ if (focusedElm instanceof ElementNode && !focusedElm.states.has('$focus')) {
41
+ focusedElm.setFocus();
42
+ props.onSelectedChanged?.call(gridRef, focusedIndex(), gridRef, focusedElm);
43
+ return true;
44
+ }
45
+ return false;
46
+ }
21
47
 
22
- const moveFocus = (delta: number, elm: ElementNode) => {
48
+ function moveFocus(delta: number) {
23
49
  if (!props.items || props.items.length === 0) return false;
24
50
  const newIndex = focusedIndex() + delta;
25
51
 
@@ -37,13 +63,10 @@ export const Grid = <T,>(props: {
37
63
  } else {
38
64
  return false;
39
65
  }
40
- const focusedElm = elm.children[focusedIndex()] as ElementNode;
41
- focusedElm.setFocus();
42
- isFunction(props.onSelectedChanged) && props.onSelectedChanged.call(elm, focusedIndex(), elm, focusedElm);
43
- return true;
66
+ return focus();
44
67
  };
45
68
 
46
- const handleHorizontalFocus = (delta: number, elm: ElementNode) => {
69
+ function handleHorizontalFocus(delta: number) {
47
70
  if (!props.items || props.items.length === 0) return false;
48
71
  const newIndex = focusedIndex() + delta;
49
72
  const isWithinRow = Math.floor(newIndex / columns()) === Math.floor(focusedIndex() / columns());
@@ -57,39 +80,41 @@ export const Grid = <T,>(props: {
57
80
  } else {
58
81
  return false;
59
82
  }
60
- const focusedElm = elm.children[focusedIndex()] as ElementNode;
61
- focusedElm.setFocus();
62
- isFunction(props.onSelectedChanged) && props.onSelectedChanged.call(elm, focusedIndex(), elm, focusedElm);
63
- return true;
83
+ return focus();
64
84
  };
65
85
 
66
- function onFocus(this: ElementNode) {
67
- handleHorizontalFocus(0, this);
68
- }
86
+ // Handle focus when items change - important for autofocus
87
+ createEffect(() => {
88
+ if (props.items && props.items.length > 0 && gridRef && gridRef.states.has('$focus')) {
89
+ queueMicrotask(focus)
90
+ }
91
+ })
69
92
 
70
93
  const scrollY = createMemo(() =>
71
94
  props.scroll === "none" ? props.y ?? 0 : -Math.floor(focusedIndex() / columns()) * totalHeight() + (props.y || 0)
72
95
  );
73
96
 
97
+ let gridRef!: ElementNode;
74
98
  return (
75
99
  <view
76
- transition={{ y: true }}
77
100
  {...props}
78
- onUp={(_e, elm) => moveFocus(-columns(), elm)}
79
- onDown={(_e, elm) => moveFocus(columns(), elm)}
80
- onLeft={(_e, elm) => handleHorizontalFocus(-1, elm)}
81
- onRight={(_e, elm) => handleHorizontalFocus(1, elm)}
82
- onFocus={onFocus}
101
+ ref={chainRefs(el => gridRef = el, props.ref)}
102
+ transition={{ y: true }}
103
+ onUp={() => moveFocus(-columns())}
104
+ onDown={() => moveFocus(columns())}
105
+ onLeft={() => handleHorizontalFocus(-1)}
106
+ onRight={() => handleHorizontalFocus(1)}
107
+ onFocus={() => handleHorizontalFocus(0)}
83
108
  strictBounds={false}
84
109
  y={scrollY()}
85
110
  >
86
111
  <For each={props.items}>
87
112
  {(item, index) => (
88
- <Dynamic
89
- {...item}
90
- component={props.item}
91
- width={props.itemWidth}
92
- height={props.itemHeight}
113
+ <props.children
114
+ item={item}
115
+ index={index()}
116
+ width={itemWidth()}
117
+ height={itemHeight()}
93
118
  x={(index() % columns()) * totalWidth()}
94
119
  y={Math.floor(index() / columns()) * totalHeight()}
95
120
  />
@@ -98,4 +123,3 @@ export const Grid = <T,>(props: {
98
123
  </view>
99
124
  );
100
125
  };
101
-
@@ -0,0 +1,36 @@
1
+ import { type Component, createRenderEffect, createSignal } from 'solid-js';
2
+ import { renderer, type NodeProps, type ImageTexture} from '@lightningtv/solid';
3
+ export interface ImageProps extends NodeProps {
4
+ src: string;
5
+ /* image to load while src is being loaded */
6
+ placeholder?: string;
7
+ fallback?: string;
8
+ }
9
+
10
+ export const Image: Component<ImageProps> = (props) => {
11
+ const [texture, setTexture] = createSignal<any>(null);
12
+ const [src, setSrc] = createSignal<string | null>(props.placeholder || null);
13
+
14
+ createRenderEffect(() => {
15
+ const srcTexture = renderer.createTexture('ImageTexture', props) as ImageTexture;
16
+
17
+ if (props.fallback) {
18
+ srcTexture.once('failed', () => {
19
+ if (props.fallback === props.placeholder) {
20
+ return;
21
+ }
22
+ setSrc(props.fallback!);
23
+ });
24
+ }
25
+
26
+ srcTexture.getTextureData().then(resp => {
27
+ // if texture fails to load, this is still called after the failed handler
28
+ if (resp.data)
29
+ setTexture(srcTexture);
30
+ })
31
+ })
32
+
33
+ return (
34
+ <view {...props} src={src()} color={props.color || 0xffffffff} texture={texture()} />
35
+ );
36
+ };
@@ -0,0 +1,124 @@
1
+ import { Route, RoutePreloadFuncArgs, RouteProps } from "@solidjs/router";
2
+ import * as s from 'solid-js';
3
+ import { ElementNode } from "@lightningtv/solid";
4
+
5
+ export interface KeepAliveElement {
6
+ id: string;
7
+ owner: s.Owner | null;
8
+ children: s.JSX.Element;
9
+ routeSignal?: s.Signal<unknown>;
10
+ dispose: () => void;
11
+ }
12
+
13
+ const keepAliveElements = new Map<string, KeepAliveElement>();
14
+
15
+ export const storeKeepAlive = (
16
+ element: KeepAliveElement
17
+ ): KeepAliveElement | undefined => {
18
+ if (keepAliveElements.has(element.id)) {
19
+ console.warn(`[KeepAlive] Element with id "${element.id}" already in cache. Recreating.`);
20
+ return element;
21
+ }
22
+ keepAliveElements.set(element.id, element);
23
+ return element;
24
+ };
25
+
26
+ export const removeKeepAlive = (id: string): void => {
27
+ const element = keepAliveElements.get(id);
28
+ if (element) {
29
+ element.dispose();
30
+ keepAliveElements.delete(id);
31
+ }
32
+ };
33
+
34
+ interface KeepAliveProps {
35
+ id: string;
36
+ shouldDispose?: (key: string) => boolean;
37
+ onRemove?: ElementNode['onRemove'];
38
+ onRender?: ElementNode['onRender'];
39
+ transition?: ElementNode['transition'];
40
+ }
41
+
42
+ function wrapChildren(props: s.ParentProps<KeepAliveProps>) {
43
+ const onRemove = props.onRemove || ((elm: ElementNode) => { elm.alpha = 0; });
44
+ const onRender = props.onRender || ((elm: ElementNode) => { elm.alpha = 1; });
45
+ const transition = props.transition || { alpha: true };
46
+
47
+ return (
48
+ <view
49
+ preserve
50
+ onRemove={onRemove}
51
+ onRender={onRender}
52
+ forwardFocus={0}
53
+ transition={transition}
54
+ {...props}
55
+ />)
56
+ }
57
+
58
+ export const KeepAlive = (props: s.ParentProps<KeepAliveProps>) => {
59
+ let existing = keepAliveElements.get(props.id)
60
+
61
+ if (existing && props.shouldDispose?.(props.id)) {
62
+ existing.dispose();
63
+ keepAliveElements.delete(props.id);
64
+ existing = undefined;
65
+ }
66
+
67
+ if (!existing) {
68
+ return s.createRoot((dispose) => {
69
+ const children = wrapChildren(props);
70
+ storeKeepAlive({
71
+ id: props.id,
72
+ owner: s.getOwner(),
73
+ children,
74
+ dispose,
75
+ });
76
+ return children;
77
+ });
78
+ } else if (existing && !existing.children) {
79
+ existing.children = s.runWithOwner(existing.owner, () => wrapChildren(props));
80
+ }
81
+ return existing.children;
82
+ };
83
+
84
+ export const KeepAliveRoute = <S extends string>(props: RouteProps<S> & {
85
+ id?: string,
86
+ path: string,
87
+ component: s.Component<RouteProps<S>>,
88
+ shouldDispose?: (key: string) => boolean,
89
+ onRemove?: ElementNode['onRemove'];
90
+ onRender?: ElementNode['onRender'];
91
+ transition?: ElementNode['transition'];
92
+ }) => {
93
+ const key = props.id || props.path;
94
+
95
+ const preload = props.preload ? (preloadProps: RoutePreloadFuncArgs) => {
96
+ let existing = keepAliveElements.get(key)
97
+
98
+ if (existing && props.shouldDispose?.(key)) {
99
+ existing.dispose();
100
+ keepAliveElements.delete(key);
101
+ existing = undefined;
102
+ }
103
+
104
+ if (!existing) {
105
+ return s.createRoot((dispose) => {
106
+ storeKeepAlive({
107
+ id: key,
108
+ owner: s.getOwner(),
109
+ dispose,
110
+ children: null,
111
+ });
112
+ return props.preload!(preloadProps);
113
+ });
114
+ } else if (existing.children) {
115
+ (existing.children as unknown as ElementNode)?.setFocus();
116
+ }
117
+ } : undefined;
118
+
119
+ return (<Route {...props} preload={preload} component={(childProps) =>
120
+ <KeepAlive id={key} onRemove={props.onRemove} onRender={props.onRender} transition={props.transition}>
121
+ {props.component(childProps)}
122
+ </KeepAlive>
123
+ }/>);
124
+ };
@@ -1,54 +1,55 @@
1
- import {
2
- Index,
3
- createEffect,
4
- createMemo,
5
- createSignal,
6
- Show,
7
- type JSX,
8
- type ValidComponent,
9
- untrack,
10
- type Accessor,
11
- } from 'solid-js';
12
- import { Dynamic, type NewOmit, scheduleTask, type NodeProps } from '@lightningtv/solid';
13
- import { Row, Column } from '@lightningtv/solid/primitives';
1
+ import * as s from 'solid-js';
2
+ import * as lng from '@lightningtv/solid';
3
+ import * as lngp from '@lightningtv/solid/primitives';
14
4
 
15
- type LazyProps<T extends readonly any[]> = NewOmit<NodeProps, 'children'> & {
5
+ type LazyProps<T extends readonly any[]> = lng.NewOmit<lng.NodeProps, 'children'> & {
16
6
  each: T | undefined | null | false;
17
- fallback?: JSX.Element;
18
7
  upCount: number;
8
+ buffer?: number;
19
9
  delay?: number;
20
10
  sync?: boolean;
21
11
  eagerLoad?: boolean;
22
- children: (item: Accessor<T[number]>, index: number) => JSX.Element;
12
+ noRefocus?: boolean;
13
+ children: (item: s.Accessor<T[number]>, index: number) => s.JSX.Element;
23
14
  };
24
15
 
25
16
  function createLazy<T>(
26
- component: ValidComponent,
17
+ component: s.ValidComponent,
27
18
  props: LazyProps<readonly T[]>,
28
- keyHandler: (updateOffset: () => void) => Record<string, () => void>
19
+ keyHandler: (updateOffset: (event: KeyboardEvent, container: lng.ElementNode) => void) => Record<string, (event: KeyboardEvent, container: lng.ElementNode) => void>
29
20
  ) {
30
21
  // Need at least one item so it can be focused
31
- const [offset, setOffset] = createSignal(1);
22
+ const [offset, setOffset] = s.createSignal<number>(props.sync ? props.upCount : 0);
32
23
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
24
+ let viewRef!: lngp.NavigableElement;
25
+ let itemLength: number = 0;
33
26
 
34
- createEffect(() => setOffset((props.selected || 0) + 1));
27
+ const buffer = s.createMemo(() => {
28
+ if (typeof props.buffer === 'number') {
29
+ return props.buffer;
30
+ }
31
+ const scroll = props.scroll || props.style?.scroll;
32
+ if (!scroll || scroll === 'auto' || scroll === 'always') return props.upCount + 1;
33
+ if (scroll === 'center') return Math.ceil(props.upCount / 2) + 1;
34
+ return 2;
35
+ });
36
+
37
+ s.createRenderEffect(() => setOffset(offset => Math.max(offset, (props.selected || 0) + buffer())));
35
38
 
36
- if (props.sync) {
37
- setOffset(props.upCount);
38
- } else {
39
- createEffect(() => {
39
+ if (!props.sync || props.eagerLoad) {
40
+ s.createEffect(() => {
40
41
  if (props.each) {
41
42
  const loadItems = () => {
42
- let count = untrack(offset);
43
+ let count = s.untrack(offset);
43
44
  if (count < props.upCount) {
44
45
  setOffset(count + 1);
45
46
  timeoutId = setTimeout(loadItems, 16); // ~60fps
46
47
  count++;
47
48
  } else if (props.eagerLoad) {
48
49
  const maxOffset = props.each ? props.each.length : 0;
49
- if (offset() >= maxOffset) return;
50
+ if (count >= maxOffset) return;
50
51
  setOffset((prev) => Math.min(prev + 1, maxOffset));
51
- scheduleTask(loadItems);
52
+ lng.scheduleTask(loadItems);
52
53
  }
53
54
  };
54
55
  loadItems();
@@ -56,11 +57,30 @@ function createLazy<T>(
56
57
  });
57
58
  }
58
59
 
59
- const items = createMemo(() => (Array.isArray(props.each) ? props.each.slice(0, offset()) : []));
60
+ const items: s.Accessor<T[]> = s.createMemo(() => {
61
+ if (Array.isArray(props.each)) {
62
+ if (itemLength != props.each.length) {
63
+ itemLength = props.each.length;
64
+ if (viewRef && !viewRef.noRefocus && lng.hasFocus(viewRef)) {
65
+ queueMicrotask(viewRef.setFocus);
66
+ }
67
+ }
68
+ return props.each.slice(0, offset());
69
+ }
70
+ itemLength = 0;
71
+ return [];
72
+ });
73
+
74
+ function lazyScrollToIndex(this: lngp.NavigableElement, index: number) {
75
+ setOffset(Math.max(index, 0) + buffer())
76
+ queueMicrotask(() => viewRef.scrollToIndex(index));
77
+ }
60
78
 
61
- const updateOffset = () => {
79
+ const updateOffset = (_event: KeyboardEvent, container: lng.ElementNode) => {
62
80
  const maxOffset = props.each ? props.each.length : 0;
63
- if (offset() >= maxOffset) return;
81
+ const selected = container.selected || 0;
82
+ const numChildren = container.children.length;
83
+ if (offset() >= maxOffset || selected < numChildren - buffer()) return;
64
84
 
65
85
  if (!props.delay) {
66
86
  setOffset((prev) => Math.min(prev + 1, maxOffset));
@@ -82,18 +102,21 @@ function createLazy<T>(
82
102
  const handler = keyHandler(updateOffset);
83
103
 
84
104
  return (
85
- <Show when={items()} fallback={props.fallback}>
86
- <Dynamic component={component} {...props} {/* @once */ ...handler}>
87
- <Index each={items()} children={props.children} />
88
- </Dynamic>
89
- </Show>
105
+ <lng.Dynamic
106
+ {...props}
107
+ component={component}
108
+ {/* @once */ ...handler}
109
+ lazyScrollToIndex={lazyScrollToIndex}
110
+ ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)} >
111
+ <s.Index each={items()} children={props.children} />
112
+ </lng.Dynamic>
90
113
  );
91
114
  }
92
115
 
93
116
  export function LazyRow<T extends readonly any[]>(props: LazyProps<T>) {
94
- return createLazy(Row, props, (updateOffset) => ({ onRight: updateOffset }));
117
+ return createLazy(lngp.Row, props, (updateOffset) => ({ onRight: updateOffset }));
95
118
  }
96
119
 
97
120
  export function LazyColumn<T extends readonly any[]>(props: LazyProps<T>) {
98
- return createLazy(Column, props, (updateOffset) => ({ onDown: updateOffset }));
121
+ return createLazy(lngp.Column, props, (updateOffset) => ({ onDown: updateOffset }));
99
122
  }
@@ -0,0 +1,149 @@
1
+ import * as s from 'solid-js'
2
+ import * as lng from '@lightningtv/solid'
3
+ import { chainFunctions } from './utils/chainFunctions.js'
4
+
5
+ export interface MarqueeAnimationProps {
6
+ /** delay in ms between animations, @default 1000 */
7
+ delay?: number
8
+ /** pixels per second, @default 200 */
9
+ speed?: number
10
+ /**
11
+ * distance between the end of the text and the start of the next animation
12
+ * @default `0.5 * clipWidth`
13
+ */
14
+ scrollGap?: number
15
+ /** easing function, @default 'linear' */
16
+ easing?: string
17
+ }
18
+
19
+ export interface MarqueeControlProps {
20
+ /** whether to scroll the text or show it contained */
21
+ marquee: boolean
22
+ }
23
+
24
+ export interface MarqueeTextProps
25
+ extends lng.TextProps, MarqueeControlProps, MarqueeAnimationProps {
26
+ /** width of the container */
27
+ clipWidth: number
28
+ }
29
+
30
+ export interface MarqueeProps
31
+ extends lng.NewOmit<lng.NodeProps, 'children'>, MarqueeControlProps, MarqueeAnimationProps {
32
+ textProps?: lng.TextProps
33
+ children: lng.TextProps['children']
34
+ }
35
+
36
+ const SAFETY_MARGIN = 10
37
+
38
+ /**
39
+ * MarqueeText is a component that scrolls text when it overflows the container.
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * <view width={400} height={28} clipping>
44
+ * <MarqueeText
45
+ * clipWidth={400}
46
+ * marquee={inFocus()}
47
+ * speed={200}
48
+ * delay={1000}
49
+ * scrollGap={24}
50
+ * easing='ease-in-out'
51
+ * >
52
+ * This is a long text that will scroll when it overflows the container.
53
+ * </MarqueeText>
54
+ * </view>
55
+ */
56
+ export function MarqueeText(props: MarqueeTextProps) {
57
+
58
+ const speed = s.createMemo(() => props.speed || 200)
59
+ const delay = s.createMemo(() => props.delay ?? 1000)
60
+ const scrollGap = s.createMemo(() => props.scrollGap ?? (props.clipWidth * 0.5))
61
+
62
+ const [textWidth, setTextWidth] = s.createSignal(0)
63
+
64
+ const isTextOverflowing = s.createMemo(() => textWidth() > props.clipWidth - SAFETY_MARGIN)
65
+ const shouldScroll = s.createMemo(() => props.marquee && isTextOverflowing())
66
+
67
+ const wasFocusedBefore = s.createMemo<boolean>(p => p || props.marquee, false)
68
+
69
+ s.createEffect(() => {
70
+ if (shouldScroll()) {
71
+
72
+ let options: lng.AnimationSettings = {
73
+ duration: (textWidth() + scrollGap()) / speed() * 1000,
74
+ delay: delay(),
75
+ loop: true,
76
+ easing: props.easing,
77
+ }
78
+
79
+ text1.lng.x = 0
80
+ text2.lng.x = textWidth() + scrollGap()
81
+
82
+ let a1 = text1.lng.animate!({x: -textWidth() -scrollGap()}, options).start()
83
+ let a2 = text2.lng.animate!({x: 0}, options).start()
84
+
85
+ s.onCleanup(() => {
86
+ a1.stop()
87
+ a2.stop()
88
+ })
89
+ }
90
+ })
91
+
92
+ const events = {loaded(el: lng.ElementNode) {setTextWidth(el.width)}}
93
+
94
+ let text1!: lng.ElementNode
95
+ let text2!: lng.ElementNode
96
+ return (
97
+ <>
98
+ {wasFocusedBefore() && <>
99
+ <text {...props} ref={text1} hidden={!shouldScroll()} rtt maxLines={1} onEvent={events} />
100
+ <text {...props} ref={text2} hidden={!shouldScroll()} rtt maxLines={1} />
101
+ </>}
102
+ <text {...props} maxLines={1} hidden={shouldScroll()} contain='width' />
103
+ </>
104
+ )
105
+ }
106
+
107
+ /**
108
+ * Marquee is a component that scrolls text when it overflows the container.
109
+ * It uses the {@link MarqueeText} component to do the actual scrolling.
110
+ *
111
+ * @example
112
+ * ```tsx
113
+ * <Marquee
114
+ * width={400}
115
+ * marquee={inFocus()}
116
+ * easing='ease-in-out'
117
+ * textProps={{
118
+ * fontSize: 28,
119
+ * }}
120
+ * >
121
+ * This is a long text that will scroll when it overflows the container.
122
+ * </Marquee>
123
+ * ```
124
+ */
125
+ export function Marquee(props: MarqueeProps) {
126
+ const [clipWidth, setClipWidth] = s.createSignal(props.width || 0);
127
+ const clipHeight = s.createMemo(() => props.height || props.textProps?.lineHeight || ((props.textProps?.fontSize || 16) * 1.5));
128
+
129
+ return (
130
+ <view
131
+ {...props}
132
+ height={clipHeight()}
133
+ onLayout={/* @once */ chainFunctions(props.onLayout, (e: lng.ElementNode) => setClipWidth(e.width))}
134
+ clipping={props.marquee}
135
+ >
136
+ <MarqueeText
137
+ {...props.textProps}
138
+ marquee={props.marquee}
139
+ clipWidth={clipWidth()}
140
+ speed={props.speed}
141
+ delay={props.delay}
142
+ scrollGap={props.scrollGap}
143
+ easing={props.easing}
144
+ >
145
+ {props.children}
146
+ </MarqueeText>
147
+ </view>
148
+ )
149
+ }
@@ -0,0 +1,18 @@
1
+ import * as s from 'solid-js'
2
+ import * as lng from '@lightningtv/solid'
3
+
4
+ function Preserve(props: lng.NodeProps): s.JSX.Element {
5
+
6
+ let view = <view {...props} /> as any as lng.ElementNode
7
+
8
+ view.preserve = true;
9
+
10
+ view.onRender ??= () => {view.hidden = false}
11
+ view.onRemove ??= () => {view.hidden = true}
12
+
13
+ s.onCleanup(() => {view.destroy()})
14
+
15
+ return view as any as s.JSX.Element
16
+ }
17
+
18
+ export default Preserve;
@@ -1,12 +1,12 @@
1
1
  import { type Component } from 'solid-js';
2
- import { combineStyles, type NodeStyles, View, type ElementNode } from '@lightningtv/solid';
2
+ import { combineStyles, type NodeStyles, type ElementNode } from '@lightningtv/solid';
3
3
  import { chainFunctions } from './utils/chainFunctions.js';
4
4
  import {
5
5
  handleNavigation,
6
- onGridFocus,
6
+ navigableForwardFocus
7
7
  } from './utils/handleNavigation.js';
8
- import { withScrolling } from './utils/withScrolling.js';
9
8
  import type { RowProps } from './types.js';
9
+ import { scrollRow } from './utils/withScrolling.js';
10
10
 
11
11
  const RowStyles: NodeStyles = {
12
12
  display: 'flex',
@@ -19,16 +19,15 @@ const RowStyles: NodeStyles = {
19
19
  },
20
20
  };
21
21
 
22
- const onLeft = handleNavigation('left');
23
- const onRight = handleNavigation('right');
24
- const scroll = withScrolling(true);
25
-
26
22
  function scrollToIndex(this: ElementNode, index: number) {
27
23
  this.selected = index;
28
- scroll(index, this);
29
- this.setFocus();
24
+ scrollRow(index, this);
25
+ this.children[index]?.setFocus();
30
26
  }
31
27
 
28
+ const onLeft = handleNavigation('left');
29
+ const onRight = handleNavigation('right');
30
+
32
31
  export const Row: Component<RowProps> = (props) => {
33
32
  return (
34
33
  <view
@@ -36,16 +35,16 @@ export const Row: Component<RowProps> = (props) => {
36
35
  selected={props.selected || 0}
37
36
  onLeft={/* @once */ chainFunctions(props.onLeft, onLeft)}
38
37
  onRight={/* @once */ chainFunctions(props.onRight, onRight)}
39
- forwardFocus={/* once */ onGridFocus(props.onSelectedChanged)}
38
+ forwardFocus={navigableForwardFocus}
40
39
  scrollToIndex={scrollToIndex}
41
40
  onLayout={
42
41
  /* @once */
43
- props.selected ? chainFunctions(props.onLayout, scroll) : props.onLayout
42
+ props.selected ? chainFunctions(props.onLayout, scrollRow) : props.onLayout
44
43
  }
45
44
  onSelectedChanged={
46
45
  /* @once */ chainFunctions(
47
46
  props.onSelectedChanged,
48
- props.scroll !== 'none' ? scroll : undefined,
47
+ props.scroll !== 'none' ? scrollRow : undefined,
49
48
  )
50
49
  }
51
50
  style={/* @once */ combineStyles(props.style, RowStyles)}