@lightningtv/solid 3.0.0-9 → 3.0.1
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/LICENSE +1 -1
- package/README.md +6 -0
- package/dist/jsx-runtime.d.ts +14 -0
- package/dist/src/activeElement.d.ts +1 -1
- package/dist/src/core/animation.d.ts +35 -0
- package/dist/src/core/animation.js +119 -0
- package/dist/src/core/animation.js.map +1 -0
- package/dist/src/core/config.d.ts +49 -0
- package/dist/src/core/config.js +33 -0
- package/dist/src/core/config.js.map +1 -0
- package/dist/src/core/domRenderer.d.ts +115 -0
- package/dist/src/core/domRenderer.js +1152 -0
- package/dist/src/core/domRenderer.js.map +1 -0
- package/dist/src/core/elementNode.d.ts +463 -0
- package/dist/src/core/elementNode.js +833 -0
- package/dist/src/core/elementNode.js.map +1 -0
- package/dist/src/core/flex.d.ts +2 -0
- package/dist/src/core/flex.js +243 -0
- package/dist/src/core/flex.js.map +1 -0
- package/dist/src/core/focusKeyTypes.d.ts +42 -0
- package/dist/src/core/focusKeyTypes.js +2 -0
- package/dist/src/core/focusKeyTypes.js.map +1 -0
- package/dist/src/core/focusManager.d.ts +13 -0
- package/dist/src/core/focusManager.js +276 -0
- package/dist/src/core/focusManager.js.map +1 -0
- package/dist/src/core/index.d.ts +12 -0
- package/dist/src/core/index.js +12 -0
- package/dist/src/core/index.js.map +1 -0
- package/dist/src/core/intrinsicTypes.d.ts +90 -0
- package/dist/src/core/intrinsicTypes.js +2 -0
- package/dist/src/core/intrinsicTypes.js.map +1 -0
- package/dist/src/core/lightningInit.d.ts +89 -0
- package/dist/src/core/lightningInit.js +26 -0
- package/dist/src/core/lightningInit.js.map +1 -0
- package/dist/src/core/nodeTypes.d.ts +6 -0
- package/dist/src/core/nodeTypes.js +6 -0
- package/dist/src/core/nodeTypes.js.map +1 -0
- package/dist/src/core/shaders.d.ts +51 -0
- package/dist/src/core/shaders.js +446 -0
- package/dist/src/core/shaders.js.map +1 -0
- package/dist/src/core/states.d.ts +12 -0
- package/dist/src/core/states.js +84 -0
- package/dist/src/core/states.js.map +1 -0
- package/dist/src/core/utils.d.ts +39 -0
- package/dist/src/core/utils.js +164 -0
- package/dist/src/core/utils.js.map +1 -0
- package/dist/src/devtools/index.d.ts +1 -1
- package/dist/src/devtools/index.js +1 -1
- package/dist/src/devtools/index.js.map +1 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/primitives/Column.jsx +9 -10
- package/dist/src/primitives/Column.jsx.map +1 -1
- package/dist/src/primitives/FPSCounter.jsx +15 -2
- package/dist/src/primitives/FPSCounter.jsx.map +1 -1
- package/dist/src/primitives/Image.d.ts +8 -0
- package/dist/src/primitives/Image.jsx +24 -0
- package/dist/src/primitives/Image.jsx.map +1 -0
- package/dist/src/primitives/KeepAlive.d.ts +30 -0
- package/dist/src/primitives/KeepAlive.jsx +77 -0
- package/dist/src/primitives/KeepAlive.jsx.map +1 -0
- package/dist/src/primitives/Lazy.d.ts +8 -7
- package/dist/src/primitives/Lazy.jsx +52 -20
- package/dist/src/primitives/Lazy.jsx.map +1 -1
- package/dist/src/primitives/LazyImport.d.ts +8 -0
- package/dist/src/primitives/LazyImport.js +40 -0
- package/dist/src/primitives/LazyImport.js.map +1 -0
- package/dist/src/primitives/Preserve.d.ts +4 -0
- package/dist/src/primitives/Preserve.jsx +11 -0
- package/dist/src/primitives/Preserve.jsx.map +1 -0
- package/dist/src/primitives/Row.jsx +9 -10
- package/dist/src/primitives/Row.jsx.map +1 -1
- package/dist/src/primitives/Suspense.d.ts +22 -0
- package/dist/src/primitives/Suspense.jsx +33 -0
- package/dist/src/primitives/Suspense.jsx.map +1 -0
- package/dist/src/primitives/Virtual.d.ts +18 -0
- package/dist/src/primitives/Virtual.jsx +443 -0
- package/dist/src/primitives/Virtual.jsx.map +1 -0
- package/dist/src/primitives/VirtualGrid.d.ts +13 -0
- package/dist/src/primitives/VirtualGrid.jsx +160 -0
- package/dist/src/primitives/VirtualGrid.jsx.map +1 -0
- package/dist/src/primitives/Visible.d.ts +0 -1
- package/dist/src/primitives/Visible.jsx +1 -1
- package/dist/src/primitives/Visible.jsx.map +1 -1
- package/dist/src/primitives/announcer/announcer.d.ts +1 -0
- package/dist/src/primitives/announcer/announcer.js +4 -3
- package/dist/src/primitives/announcer/announcer.js.map +1 -1
- package/dist/src/primitives/announcer/speech.d.ts +1 -1
- package/dist/src/primitives/announcer/speech.js +98 -8
- package/dist/src/primitives/announcer/speech.js.map +1 -1
- package/dist/src/primitives/createFocusStack.d.ts +4 -4
- package/dist/src/primitives/createFocusStack.jsx +15 -6
- package/dist/src/primitives/createFocusStack.jsx.map +1 -1
- package/dist/src/primitives/createTag.d.ts +8 -0
- package/dist/src/primitives/createTag.jsx +20 -0
- package/dist/src/primitives/createTag.jsx.map +1 -0
- package/dist/src/primitives/index.d.ts +13 -4
- package/dist/src/primitives/index.js +12 -3
- package/dist/src/primitives/index.js.map +1 -1
- package/dist/src/primitives/types.d.ts +3 -2
- package/dist/src/primitives/useFocusManager.d.ts +2 -2
- package/dist/src/primitives/useFocusManager.js +2 -2
- package/dist/src/primitives/useFocusManager.js.map +1 -1
- package/dist/src/primitives/useMouse.d.ts +18 -2
- package/dist/src/primitives/useMouse.js +171 -47
- package/dist/src/primitives/useMouse.js.map +1 -1
- package/dist/src/primitives/utils/createBlurredImage.d.ts +56 -0
- package/dist/src/primitives/utils/createBlurredImage.js +223 -0
- package/dist/src/primitives/utils/createBlurredImage.js.map +1 -0
- package/dist/src/primitives/utils/createSpriteMap.d.ts +2 -2
- package/dist/src/primitives/utils/createSpriteMap.js +3 -3
- package/dist/src/primitives/utils/createSpriteMap.js.map +1 -1
- package/dist/src/primitives/utils/handleNavigation.d.ts +79 -5
- package/dist/src/primitives/utils/handleNavigation.js +241 -69
- package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
- package/dist/src/primitives/utils/withScrolling.d.ts +12 -2
- package/dist/src/primitives/utils/withScrolling.js +59 -7
- package/dist/src/primitives/utils/withScrolling.js.map +1 -1
- package/dist/src/render.d.ts +5 -4
- package/dist/src/render.js +5 -1
- package/dist/src/render.js.map +1 -1
- package/dist/src/shaders/Rounded.d.ts +7 -0
- package/dist/src/shaders/Rounded.js +88 -0
- package/dist/src/shaders/Rounded.js.map +1 -0
- package/dist/src/shaders/RoundedWithBorder.d.ts +3 -0
- package/dist/src/shaders/RoundedWithBorder.js +217 -0
- package/dist/src/shaders/RoundedWithBorder.js.map +1 -0
- package/dist/src/shaders/index.d.ts +4 -0
- package/dist/src/shaders/index.js +5 -0
- package/dist/src/shaders/index.js.map +1 -0
- package/dist/src/shaders/templates/RoundedTemplate.d.ts +12 -0
- package/dist/src/shaders/templates/RoundedTemplate.js +48 -0
- package/dist/src/shaders/templates/RoundedTemplate.js.map +1 -0
- package/dist/src/shaders/templates/RoundedWithBorderTemplate.d.ts +20 -0
- package/dist/src/shaders/templates/RoundedWithBorderTemplate.js +93 -0
- package/dist/src/shaders/templates/RoundedWithBorderTemplate.js.map +1 -0
- package/dist/src/shaders/utils.d.ts +3 -0
- package/dist/src/shaders/utils.js +31 -0
- package/dist/src/shaders/utils.js.map +1 -0
- package/dist/src/solidOpts.d.ts +1 -7
- package/dist/src/solidOpts.js +9 -1
- package/dist/src/solidOpts.js.map +1 -1
- package/dist/src/types.d.ts +1 -13
- package/dist/src/utils.d.ts +3 -1
- package/dist/src/utils.js +9 -1
- package/dist/src/utils.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/jsx-runtime.d.ts +1 -1
- package/package.json +28 -16
- package/src/activeElement.ts +1 -1
- package/src/core/animation.ts +185 -0
- package/src/core/config.ts +89 -0
- package/src/core/domRenderer.ts +1300 -0
- package/src/core/elementNode.ts +1458 -0
- package/src/core/flex.ts +284 -0
- package/src/core/focusKeyTypes.ts +90 -0
- package/src/core/focusManager.ts +381 -0
- package/src/core/index.ts +13 -0
- package/src/core/intrinsicTypes.ts +199 -0
- package/src/core/lightningInit.ts +147 -0
- package/src/core/nodeTypes.ts +6 -0
- package/src/core/shaders.ts +567 -0
- package/src/core/states.ts +91 -0
- package/src/core/utils.ts +222 -0
- package/src/devtools/index.ts +1 -1
- package/src/index.ts +3 -3
- package/src/primitives/Column.tsx +10 -12
- package/src/primitives/FPSCounter.tsx +16 -2
- package/src/primitives/Image.tsx +36 -0
- package/src/primitives/KeepAlive.tsx +124 -0
- package/src/primitives/Lazy.tsx +66 -37
- package/src/primitives/LazyImport.ts +53 -0
- package/src/primitives/Preserve.tsx +18 -0
- package/src/primitives/Row.tsx +13 -14
- package/src/primitives/Suspense.tsx +39 -0
- package/src/primitives/Virtual.tsx +486 -0
- package/src/primitives/VirtualGrid.tsx +220 -0
- package/src/primitives/Visible.tsx +1 -2
- package/src/primitives/announcer/announcer.ts +10 -3
- package/src/primitives/announcer/speech.ts +113 -6
- package/src/primitives/createFocusStack.tsx +18 -7
- package/src/primitives/createTag.tsx +33 -0
- package/src/primitives/index.ts +13 -4
- package/src/primitives/types.ts +3 -2
- package/src/primitives/useFocusManager.ts +3 -3
- package/src/primitives/useMouse.ts +306 -67
- package/src/primitives/utils/createBlurredImage.ts +366 -0
- package/src/primitives/utils/createSpriteMap.ts +8 -6
- package/src/primitives/utils/handleNavigation.ts +300 -84
- package/src/primitives/utils/withScrolling.ts +76 -18
- package/src/render.ts +7 -3
- package/src/shaders/Rounded.ts +100 -0
- package/src/shaders/RoundedWithBorder.ts +245 -0
- package/src/shaders/index.ts +4 -0
- package/src/shaders/templates/RoundedTemplate.ts +57 -0
- package/src/shaders/templates/RoundedWithBorderTemplate.ts +110 -0
- package/src/shaders/utils.ts +44 -0
- package/src/solidOpts.ts +9 -7
- package/src/types.ts +1 -15
- package/src/utils.ts +11 -1
- package/dist/src/client.d.ts +0 -1
- package/dist/src/client.js +0 -2
- package/dist/src/client.js.map +0 -1
- package/dist/src/core.d.ts +0 -1
- package/dist/src/core.js +0 -3
- package/dist/src/core.js.map +0 -1
- package/dist/src/jsx-runtime.d.ts +0 -10
- package/dist/src/jsx-runtime.js +0 -2
- package/dist/src/jsx-runtime.js.map +0 -1
- package/dist/src/primitives/Infinite.d.ts +0 -15
- package/dist/src/primitives/Infinite.jsx +0 -59
- package/dist/src/primitives/Infinite.jsx.map +0 -1
- package/dist/src/primitives/LazyUp.d.ts +0 -11
- package/dist/src/primitives/LazyUp.jsx +0 -38
- package/dist/src/primitives/LazyUp.jsx.map +0 -1
- package/dist/src/primitives/sprite.d.ts +0 -9
- package/dist/src/primitives/sprite.js +0 -18
- package/dist/src/primitives/sprite.js.map +0 -1
- package/dist/src/primitives/utils/createFocusStack.d.ts +0 -24
- package/dist/src/primitives/utils/createFocusStack.js +0 -59
- package/dist/src/primitives/utils/createFocusStack.js.map +0 -1
- package/dist/src/primitives/utils/scrollToIndex.d.ts +0 -2
- package/dist/src/primitives/utils/scrollToIndex.js +0 -33
- package/dist/src/primitives/utils/scrollToIndex.js.map +0 -1
- package/dist/src/renderClient.d.ts +0 -21
- package/dist/src/renderClient.js +0 -64
- package/dist/src/renderClient.js.map +0 -1
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import * as s from 'solid-js';
|
|
2
|
+
import * as lng from '@lightningtv/solid';
|
|
3
|
+
import * as lngp from '@lightningtv/solid/primitives';
|
|
4
|
+
import { List } from '@solid-primitives/list';
|
|
5
|
+
import * as utils from '../utils.js';
|
|
6
|
+
|
|
7
|
+
const columnScroll = lngp.withScrolling(false);
|
|
8
|
+
|
|
9
|
+
const rowStyles: lng.NodeStyles = {
|
|
10
|
+
display: 'flex',
|
|
11
|
+
flexWrap: 'wrap',
|
|
12
|
+
transition: {
|
|
13
|
+
y: true,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type VirtualGridProps<T> = lng.NewOmit<lngp.RowProps, 'children'> & {
|
|
18
|
+
each: readonly T[] | undefined | null | false;
|
|
19
|
+
columns: number; // items per row
|
|
20
|
+
rows?: number; // number of visible rows (default: 1)
|
|
21
|
+
buffer?: number;
|
|
22
|
+
onEndReached?: () => void;
|
|
23
|
+
onEndReachedThreshold?: number;
|
|
24
|
+
children: (item: s.Accessor<T>, index: s.Accessor<number>) => s.JSX.Element;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function VirtualGrid<T>(props: VirtualGridProps<T>): s.JSX.Element {
|
|
28
|
+
const bufferSize = () => props.buffer ?? 2;
|
|
29
|
+
const [ cursor, setCursor ] = s.createSignal(props.selected ?? 0);
|
|
30
|
+
const items = s.createMemo(() => props.each || []);
|
|
31
|
+
const itemCount = () => items().length;
|
|
32
|
+
const itemsPerRow = () => props.columns;
|
|
33
|
+
const numberOfRows = () => props.rows ?? 1;
|
|
34
|
+
const totalVisibleItems = () => itemsPerRow() * numberOfRows();
|
|
35
|
+
|
|
36
|
+
const start = s.createMemo(() => {
|
|
37
|
+
const perRow = itemsPerRow();
|
|
38
|
+
const newRowIndex = Math.floor(cursor() / perRow);
|
|
39
|
+
const rawStart = newRowIndex * perRow - bufferSize() * perRow;
|
|
40
|
+
return Math.max(0, rawStart);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const end = s.createMemo(() => {
|
|
44
|
+
const perRow = itemsPerRow();
|
|
45
|
+
const newRowIndex = Math.floor(cursor() / perRow);
|
|
46
|
+
const rawEnd = (newRowIndex + bufferSize()) * perRow + totalVisibleItems();
|
|
47
|
+
return Math.min(items().length, rawEnd);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const [slice, setSlice] = s.createSignal(items().slice(start(), end()));
|
|
51
|
+
|
|
52
|
+
let viewRef!: lngp.NavigableElement;
|
|
53
|
+
|
|
54
|
+
function onVerticalNav(dir: -1 | 1): lngp.KeyHandler {
|
|
55
|
+
return function () {
|
|
56
|
+
const perRow = itemsPerRow();
|
|
57
|
+
const currentRowIndex = Math.floor(cursor() / perRow);
|
|
58
|
+
const maxRows = Math.floor(items().length / perRow);
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
currentRowIndex === 0 && dir === -1
|
|
62
|
+
|| currentRowIndex === maxRows && dir === 1
|
|
63
|
+
) return;
|
|
64
|
+
|
|
65
|
+
const selected = this.selected || 0;
|
|
66
|
+
const offset = dir * perRow;
|
|
67
|
+
const newIndex = utils.clamp(selected + offset, 0, items().length - 1);
|
|
68
|
+
const lastIdx = selected;
|
|
69
|
+
this.selected = newIndex;
|
|
70
|
+
const active = this.children[this.selected];
|
|
71
|
+
|
|
72
|
+
if (active instanceof lng.ElementNode) {
|
|
73
|
+
active.setFocus();
|
|
74
|
+
chainedOnSelectedChanged.call(
|
|
75
|
+
this as lngp.NavigableElement,
|
|
76
|
+
this.selected,
|
|
77
|
+
this as lngp.NavigableElement,
|
|
78
|
+
active,
|
|
79
|
+
lastIdx
|
|
80
|
+
);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const onUp = onVerticalNav(-1);
|
|
87
|
+
const onDown = onVerticalNav(1);
|
|
88
|
+
|
|
89
|
+
const onSelectedChanged: lngp.OnSelectedChanged = function (_idx, elm, active, _lastIdx,) {
|
|
90
|
+
let idx = _idx;
|
|
91
|
+
let lastIdx = _lastIdx;
|
|
92
|
+
const perRow = itemsPerRow();
|
|
93
|
+
const newRowIndex = Math.floor(idx / perRow);
|
|
94
|
+
const prevRowIndex = Math.floor((lastIdx || 0) / perRow);
|
|
95
|
+
const prevStart = start();
|
|
96
|
+
|
|
97
|
+
setCursor(prevStart + idx);
|
|
98
|
+
if (newRowIndex === prevRowIndex) return;
|
|
99
|
+
|
|
100
|
+
setSlice(items().slice(start(), end()));
|
|
101
|
+
|
|
102
|
+
// this.selected is relative to the slice
|
|
103
|
+
// and it doesn't get corrected automatically after children change
|
|
104
|
+
const idxCorrection = prevStart - start();
|
|
105
|
+
if (lastIdx) lastIdx += idxCorrection;
|
|
106
|
+
idx += idxCorrection;
|
|
107
|
+
this.selected += idxCorrection;
|
|
108
|
+
|
|
109
|
+
if (props.onEndReachedThreshold !== undefined && cursor() >= items().length - props.onEndReachedThreshold) {
|
|
110
|
+
props.onEndReached?.();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
queueMicrotask(() => {
|
|
114
|
+
const prevRowY = this.y + active.y;
|
|
115
|
+
this.updateLayout();
|
|
116
|
+
this.lng.y = prevRowY - active.y;
|
|
117
|
+
columnScroll(idx, elm, active, lastIdx);
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const chainedOnSelectedChanged = lngp.chainFunctions(props.onSelectedChanged, onSelectedChanged)!;
|
|
122
|
+
|
|
123
|
+
let cachedSelected: number | undefined;
|
|
124
|
+
const updateSelected = ([selected, _items]: [number?, any?]) => {
|
|
125
|
+
if (!viewRef || selected == null) return;
|
|
126
|
+
|
|
127
|
+
if (cachedSelected !== undefined) {
|
|
128
|
+
selected = cachedSelected;
|
|
129
|
+
cachedSelected = undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (selected >= items().length && props.onEndReached) {
|
|
133
|
+
props.onEndReached?.();
|
|
134
|
+
cachedSelected = selected;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const item = items()[selected];
|
|
139
|
+
let active = viewRef.children.find(x => x.item === item);
|
|
140
|
+
const lastSelected = viewRef.selected;
|
|
141
|
+
|
|
142
|
+
if (active instanceof lng.ElementNode) {
|
|
143
|
+
viewRef.selected = viewRef.children.indexOf(active);
|
|
144
|
+
if (lng.hasFocus(viewRef)) {
|
|
145
|
+
// force focus as scrollToIndex is manually called
|
|
146
|
+
active.setFocus();
|
|
147
|
+
}
|
|
148
|
+
chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
|
|
149
|
+
} else {
|
|
150
|
+
setCursor(selected);
|
|
151
|
+
setSlice(items().slice(start(), end()));
|
|
152
|
+
|
|
153
|
+
queueMicrotask(() => {
|
|
154
|
+
viewRef.updateLayout();
|
|
155
|
+
active = viewRef.children.find(x => x.item === item);
|
|
156
|
+
if (active instanceof lng.ElementNode) {
|
|
157
|
+
viewRef.selected = viewRef.children.indexOf(active);
|
|
158
|
+
if (lng.hasFocus(viewRef)) {
|
|
159
|
+
active.setFocus();
|
|
160
|
+
}
|
|
161
|
+
chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const scrollToIndex = (index: number) => {
|
|
168
|
+
s.untrack(() => updateSelected([index]));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
s.createEffect(s.on([() => props.selected, items], updateSelected));
|
|
172
|
+
|
|
173
|
+
s.createEffect(
|
|
174
|
+
s.on(items, (gridItems, _prevGridItems, prevSize) => {
|
|
175
|
+
if (!viewRef) return;
|
|
176
|
+
|
|
177
|
+
if (cachedSelected !== undefined) {
|
|
178
|
+
// This occurs when VG is reloaded and user wants to select a paginated item
|
|
179
|
+
updateSelected([cachedSelected]);
|
|
180
|
+
return gridItems.length;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (gridItems.length === 0) {
|
|
184
|
+
setCursor(0);
|
|
185
|
+
cachedSelected = undefined;
|
|
186
|
+
setSlice([]);
|
|
187
|
+
} else if (cursor() >= itemCount()) {
|
|
188
|
+
updateSelected([Math.max(0, itemCount() - 1)]);
|
|
189
|
+
} else if (prevSize === 0) {
|
|
190
|
+
updateSelected([0]);
|
|
191
|
+
} else {
|
|
192
|
+
setSlice(items().slice(start(), end()));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return gridItems.length;
|
|
196
|
+
}, { defer: true })
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<view
|
|
202
|
+
{...props}
|
|
203
|
+
scroll={props.scroll || 'always'}
|
|
204
|
+
ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)}
|
|
205
|
+
selected={props.selected || 0}
|
|
206
|
+
cursor={cursor()}
|
|
207
|
+
onLeft={/* @once */ lngp.chainFunctions(props.onLeft, lngp.navigableHandleNavigation)}
|
|
208
|
+
onRight={/* @once */ lngp.chainFunctions(props.onRight, lngp.navigableHandleNavigation)}
|
|
209
|
+
onUp={/* @once */ lngp.chainFunctions(props.onUp, onUp)}
|
|
210
|
+
onDown={/* @once */ lngp.chainFunctions(props.onDown, onDown)}
|
|
211
|
+
forwardFocus={/* @once */ lngp.navigableForwardFocus}
|
|
212
|
+
onCreate={/* @once */ props.selected ? lngp.chainFunctions(props.onCreate, columnScroll) : props.onCreate}
|
|
213
|
+
scrollToIndex={/* @once */ scrollToIndex}
|
|
214
|
+
onSelectedChanged={/* @once */ chainedOnSelectedChanged}
|
|
215
|
+
style={/* @once */ lng.combineStyles(props.style, rowStyles)}
|
|
216
|
+
>
|
|
217
|
+
<List each={slice()}>{props.children}</List>
|
|
218
|
+
</view>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
@@ -13,7 +13,6 @@ import { ElementNode } from '@lightningtv/solid';
|
|
|
13
13
|
export function Visible<T>(props: {
|
|
14
14
|
when: T | undefined | null | false;
|
|
15
15
|
keyed?: boolean;
|
|
16
|
-
fallback?: JSX.Element;
|
|
17
16
|
children: JSX.Element;
|
|
18
17
|
}): JSX.Element {
|
|
19
18
|
let child: ChildrenReturn | undefined;
|
|
@@ -55,6 +54,6 @@ export function Visible<T>(props: {
|
|
|
55
54
|
}
|
|
56
55
|
});
|
|
57
56
|
|
|
58
|
-
return c ? child :
|
|
57
|
+
return c || child ? child : null;
|
|
59
58
|
}) as unknown as JSX.Element;
|
|
60
59
|
};
|
|
@@ -109,18 +109,24 @@ function onFocusChangeCore(focusPath: ElementNode[] = []) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
function textToSpeech(
|
|
112
|
+
function textToSpeech(
|
|
113
|
+
toSpeak: SpeechType,
|
|
114
|
+
aria: boolean,
|
|
115
|
+
lang: string,
|
|
116
|
+
voice?: string,
|
|
117
|
+
) {
|
|
113
118
|
if (voiceOutDisabled) {
|
|
114
119
|
return;
|
|
115
120
|
}
|
|
116
121
|
|
|
117
|
-
return (currentlySpeaking = SpeechEngine(toSpeak, lang, voice));
|
|
122
|
+
return (currentlySpeaking = SpeechEngine(toSpeak, aria, lang, voice));
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
export interface Announcer {
|
|
121
126
|
debug: boolean;
|
|
122
127
|
enabled: boolean;
|
|
123
128
|
lang: string;
|
|
129
|
+
aria: boolean;
|
|
124
130
|
voice?: string;
|
|
125
131
|
cancel: VoidFunction;
|
|
126
132
|
clearPrevFocus: (depth?: number) => void;
|
|
@@ -140,6 +146,7 @@ export const Announcer: Announcer = {
|
|
|
140
146
|
debug: false,
|
|
141
147
|
enabled: true,
|
|
142
148
|
lang: 'en-US',
|
|
149
|
+
aria: false,
|
|
143
150
|
cancel: function () {
|
|
144
151
|
currentlySpeaking && currentlySpeaking.cancel();
|
|
145
152
|
},
|
|
@@ -153,7 +160,7 @@ export const Announcer: Announcer = {
|
|
|
153
160
|
currentlySpeaking.append(text);
|
|
154
161
|
} else {
|
|
155
162
|
Announcer.cancel();
|
|
156
|
-
textToSpeech(text, Announcer.lang, Announcer.voice);
|
|
163
|
+
textToSpeech(text, Announcer.aria, Announcer.lang, Announcer.voice);
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
if (notification) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - `restoreFocus()`: Restores focus to the last stored element and removes it from the stack. Returns `true` if successful, `false` otherwise.
|
|
18
18
|
* - `clearFocusStack()`: Empties the focus stack.
|
|
19
19
|
*/
|
|
20
|
-
import
|
|
20
|
+
import * as s from 'solid-js';
|
|
21
21
|
import { type ElementNode } from '@lightningtv/solid';
|
|
22
22
|
|
|
23
23
|
interface FocusStackContextType {
|
|
@@ -26,13 +26,16 @@ interface FocusStackContextType {
|
|
|
26
26
|
clearFocusStack: () => void;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const FocusStackContext = createContext<FocusStackContextType | undefined>(undefined);
|
|
29
|
+
const FocusStackContext = s.createContext<FocusStackContextType | undefined>(undefined);
|
|
30
30
|
|
|
31
|
-
export function FocusStackProvider(props: { children: JSX.Element}) {
|
|
32
|
-
const [_focusStack, setFocusStack] = createSignal<ElementNode[]>([]);
|
|
31
|
+
export function FocusStackProvider(props: { children: s.JSX.Element}) {
|
|
32
|
+
const [_focusStack, setFocusStack] = s.createSignal<ElementNode[]>([]);
|
|
33
33
|
|
|
34
34
|
function storeFocus(element: ElementNode, prevElement?: ElementNode) {
|
|
35
|
-
|
|
35
|
+
const elm = prevElement || element;
|
|
36
|
+
if (elm) {
|
|
37
|
+
setFocusStack(stack => [...stack, elm]);
|
|
38
|
+
}
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
function restoreFocus(): boolean {
|
|
@@ -59,10 +62,18 @@ export function FocusStackProvider(props: { children: JSX.Element}) {
|
|
|
59
62
|
);
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
export function useFocusStack() {
|
|
63
|
-
const context = useContext(FocusStackContext);
|
|
65
|
+
export function useFocusStack(autoClear = true) {
|
|
66
|
+
const context = s.useContext(FocusStackContext);
|
|
64
67
|
if (!context) {
|
|
65
68
|
throw new Error("useFocusStack must be used within a FocusStackProvider");
|
|
66
69
|
}
|
|
70
|
+
|
|
71
|
+
if (autoClear) {
|
|
72
|
+
s.onCleanup(() => {
|
|
73
|
+
// delay clearing the focus stack so restoreFocus can happen first.
|
|
74
|
+
setTimeout(() => context.clearFocusStack(), 5);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
return context;
|
|
68
79
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
x={lng.rootNode.w - 1}
|
|
13
|
+
y={lng.rootNode.h - 1}
|
|
14
|
+
display='flex'
|
|
15
|
+
onLayout={(n) => {
|
|
16
|
+
if (n.preFlexwidth && n.width !== n.preFlexwidth) {
|
|
17
|
+
n.rtt = true;
|
|
18
|
+
setTimeout(() => setTexture(n.texture), 1);
|
|
19
|
+
}
|
|
20
|
+
}}
|
|
21
|
+
parent={lng.rootNode} children={children}
|
|
22
|
+
textureOptions={{
|
|
23
|
+
preventCleanup: true
|
|
24
|
+
}} /> as any as lng.ElementNode
|
|
25
|
+
Tag.render(false);
|
|
26
|
+
|
|
27
|
+
const TagComponent = (props: lng.NodeProps) => {
|
|
28
|
+
return <view color={0xffffffff} autosize {...props} texture={texture()} />;
|
|
29
|
+
};
|
|
30
|
+
TagComponent.destroy = () => Tag.destroy();
|
|
31
|
+
|
|
32
|
+
return TagComponent;
|
|
33
|
+
}
|
package/src/primitives/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ 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 './LazyImport.js';
|
|
8
|
+
export * from './Image.jsx';
|
|
7
9
|
export * from './Visible.jsx';
|
|
8
10
|
export * from './router.js';
|
|
9
11
|
export * from './Column.jsx';
|
|
@@ -11,17 +13,24 @@ export * from './Row.jsx';
|
|
|
11
13
|
export * from './Grid.jsx';
|
|
12
14
|
export * from './FPSCounter.jsx';
|
|
13
15
|
export * from './FadeInOut.jsx';
|
|
14
|
-
export * from './
|
|
16
|
+
export * from './Preserve.jsx';
|
|
17
|
+
export * from './Suspense.jsx';
|
|
15
18
|
export * from './Marquee.jsx';
|
|
19
|
+
export * from './createFocusStack.jsx';
|
|
16
20
|
export * from './useHold.js';
|
|
17
|
-
export
|
|
21
|
+
export * from './KeepAlive.jsx';
|
|
22
|
+
export * from './VirtualGrid.jsx';
|
|
23
|
+
export * from './Virtual.jsx';
|
|
24
|
+
export * from './utils/withScrolling.js';
|
|
25
|
+
export * from './createTag.jsx';
|
|
18
26
|
export {
|
|
19
27
|
type AnyFunction,
|
|
20
28
|
chainFunctions,
|
|
21
29
|
chainRefs,
|
|
22
30
|
} from './utils/chainFunctions.js';
|
|
23
|
-
export
|
|
31
|
+
export * from './utils/handleNavigation.js';
|
|
24
32
|
export { createSpriteMap, type SpriteDef } from './utils/createSpriteMap.js';
|
|
33
|
+
export { createBlurredImage } from './utils/createBlurredImage.js';
|
|
25
34
|
|
|
26
35
|
export type * from './types.js';
|
|
27
|
-
export type { KeyHandler } from '
|
|
36
|
+
export type { KeyHandler } from '../core/focusManager.js';
|
package/src/primitives/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ElementNode, NodeProps, NodeStyles } from '
|
|
2
|
-
import type { KeyHandler } from '
|
|
1
|
+
import type { ElementNode, NodeProps, NodeStyles } from '../index.js';
|
|
2
|
+
import type { KeyHandler } from '../core/focusManager.js';
|
|
3
3
|
|
|
4
4
|
export type OnSelectedChanged = (
|
|
5
5
|
this: NavigableElement,
|
|
@@ -54,6 +54,7 @@ export interface NavigableProps extends NodeProps {
|
|
|
54
54
|
// @ts-expect-error animationSettings is not identical - weird
|
|
55
55
|
export interface NavigableElement extends ElementNode, NavigableProps {
|
|
56
56
|
selected: number;
|
|
57
|
+
scrollToIndex: (this: NavigableElement, index: number) => void;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export interface NavigableStyleProperties {
|
|
@@ -6,13 +6,13 @@ import {
|
|
|
6
6
|
getOwner,
|
|
7
7
|
runWithOwner,
|
|
8
8
|
} from 'solid-js';
|
|
9
|
-
import { Config } from '
|
|
10
|
-
import type { ElementNode } from '
|
|
9
|
+
import { Config } from '../core/index.js';
|
|
10
|
+
import type { ElementNode } from '../core/index.js';
|
|
11
11
|
import {
|
|
12
12
|
useFocusManager as useFocusManagerCore,
|
|
13
13
|
type KeyMap,
|
|
14
14
|
type KeyHoldOptions,
|
|
15
|
-
} from '
|
|
15
|
+
} from '../core/focusManager.js';
|
|
16
16
|
import { activeElement, setActiveElement } from '../activeElement.js';
|
|
17
17
|
|
|
18
18
|
const [focusPath, setFocusPath] = createSignal<ElementNode[]>([]);
|