@lightningtv/solid 3.1.4 → 3.1.6
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/dist/src/core/elementNode.js +14 -2
- package/dist/src/core/elementNode.js.map +1 -1
- package/dist/src/core/focusManager.js +12 -22
- package/dist/src/core/focusManager.js.map +1 -1
- package/dist/src/primitives/Grid.d.ts +1 -0
- package/dist/src/primitives/Grid.jsx +16 -7
- package/dist/src/primitives/Grid.jsx.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/AI_CODING_GUIDELINES.md +82 -165
- package/package.json +1 -1
- package/src/core/elementNode.ts +15 -2
- package/src/core/focusManager.ts +12 -22
- package/src/primitives/Grid.tsx +21 -11
|
@@ -1,165 +1,81 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
This document outlines the specific constraints, properties, and layout systems available in the Lightning/Solid framework. AI agents should strictly adhere to these rules to generate correct and functional code.
|
|
4
|
-
|
|
5
|
-
## Core Principles
|
|
6
|
-
|
|
7
|
-
1. **Positioning System**:
|
|
8
|
-
|
|
9
|
-
- All nodes are effectively `position: absolute`.
|
|
10
|
-
- Positioning is controlled via `x`, `y`, `width`, `height`.
|
|
11
|
-
- **Right / Bottom**: usage of `right` and `bottom` is supported for pinning elements to the parent's edges.
|
|
12
|
-
- Setting `right` automatically implies `mountX: 1`.
|
|
13
|
-
- Setting `bottom` automatically implies `mountY: 1`.
|
|
14
|
-
- **Mounting**:
|
|
15
|
-
- Default `mount` is **0, 0** (Top-Left corner).
|
|
16
|
-
- `mount` (0-1) determines the anchor point of the element itself (0.5 = center).
|
|
17
|
-
- **Avoid changing `mount` manually** unless specifically needed for centering (0.5) or specific anchor effects.
|
|
18
|
-
- There is no "flow" layout by default unless `display: 'flex'` is explicitly used.
|
|
19
|
-
|
|
20
|
-
2. **Dimensions**:
|
|
21
|
-
|
|
22
|
-
- **Explicit Dimensions**: It is **important** to give elements explicit `width` and `height` whenever possible.
|
|
23
|
-
- **Default Dimensions**: If `width` and `height` are **not** specified, the element will inherit the **parent's width and height**. This can lead to unexpected full-screen overlays if not managed carefully.
|
|
24
|
-
|
|
25
|
-
3. **Flexbox Layout**:
|
|
26
|
-
|
|
27
|
-
- **Must** set `display: 'flex'` on a container to enable flexbox.
|
|
28
|
-
- **Important**: `padding` is a _single number_ only. There is NO `paddingLeft`, `paddingRight`, `paddingTop`, or `paddingBottom`.
|
|
29
|
-
- **Margins**: Supported on flex items via `marginTop`, `marginBottom`, `marginLeft`, `marginRight`.
|
|
30
|
-
- **Gap**: `gap`, `rowGap`, `columnGap` are supported.
|
|
31
|
-
- **Alignment**: `justifyContent`, `alignItems`, `alignSelf` are strictly typed.
|
|
32
|
-
|
|
33
|
-
4. **Styling Restrictions**:
|
|
34
|
-
|
|
35
|
-
- **No** `background`. Use `color`.
|
|
36
|
-
- **Color Format**: **Prefer Hex Strings** (e.g., `'#ff0000ff'`). **NO** named colors.
|
|
37
|
-
- **No** `border-radius`. Use `borderRadius` (number).
|
|
38
|
-
- **No** `border`. Use `border` object: `{ width: number, color: string }`.
|
|
39
|
-
- **No** `box-shadow`. Use `shadow` object.
|
|
40
|
-
- **No** CSS class names.
|
|
41
|
-
|
|
42
|
-
5. **Props vs Styles**:
|
|
43
|
-
- **Prefer Props**: Pass properties directly to the component (e.g., `<View x={10} y={10} color="#ff0000ff" />`).
|
|
44
|
-
- Avoid using the `style` prop when possible.
|
|
45
|
-
|
|
46
|
-
## Available Properties
|
|
47
|
-
|
|
48
|
-
### Layout & Positioning
|
|
49
|
-
|
|
50
|
-
| Property | Type | Notes |
|
|
51
|
-
| :-------------------------- | :------- | :----------------------------------------------------------- |
|
|
52
|
-
| `x`, `y` | `number` | Absolute position coordinates. |
|
|
53
|
-
| `right` | `number` | Distance from parent's right edge. **Implies `mountX: 1`**. |
|
|
54
|
-
| `bottom` | `number` | Distance from parent's bottom edge. **Implies `mountY: 1`**. |
|
|
55
|
-
| `width` / `w` | `number` | Explicit width. **Defaults to Parent Width** if unset. |
|
|
56
|
-
| `height` / `h` | `number` | Explicit height. **Defaults to Parent Height** if unset. |
|
|
57
|
-
| `minWidth`, `minHeight` | `number` | Minimum dimensions. |
|
|
58
|
-
| `maxWidth`, `maxHeight` | `number` | Maximum dimensions. |
|
|
59
|
-
| `mount`, `mountX`, `mountY` | `number` | Anchor point. Default **0** (Top/Left). |
|
|
60
|
-
| `pivot`, `pivotX`, `pivotY` | `number` | Pivot point. |
|
|
61
|
-
| `rotation` | `number` | Rotation in radians. |
|
|
62
|
-
| `scale`, `scaleX`, `scaleY` | `number` | Scaling factor. |
|
|
63
|
-
| `alpha` | `number` | Opacity. |
|
|
64
|
-
| `zIndex`, `zIndexLocked` | `number` | Stacking order. |
|
|
65
|
-
|
|
66
|
-
### Flexbox Container Props
|
|
67
|
-
|
|
68
|
-
_Requires `display: 'flex'`_
|
|
69
|
-
|
|
70
|
-
| Property | Values / Type | Notes |
|
|
71
|
-
| :--------------------------- | :----------------------------------------------------------------------------------------- | :-------------------- |
|
|
72
|
-
| `flexDirection` | `'row' \| 'column'` | Defaults to 'row'. |
|
|
73
|
-
| `flexWrap` | `'nowrap' \| 'wrap'` | |
|
|
74
|
-
| `justifyContent` | `'flexStart' \| 'flexEnd' \| 'center' \| 'spaceBetween' \| 'spaceAround' \| 'spaceEvenly'` | Main axis alignment. |
|
|
75
|
-
| `alignItems` | `'flexStart' \| 'flexEnd' \| 'center'` | Cross axis alignment. |
|
|
76
|
-
| `gap`, `rowGap`, `columnGap` | `number` | Space between items. |
|
|
77
|
-
| `padding` | `number` | **Uniform only**. |
|
|
78
|
-
|
|
79
|
-
### Flexbox Item Props
|
|
80
|
-
|
|
81
|
-
| Property | Type | Notes |
|
|
82
|
-
| :----------------------------- | :------------------------------------- | :---------------------- |
|
|
83
|
-
| `flexGrow` | `number` | |
|
|
84
|
-
| `flexItem` | `boolean` | Set `false` to ignore. |
|
|
85
|
-
| `alignSelf` | `'flexStart' \| 'flexEnd' \| 'center'` | Overrides `alignItems`. |
|
|
86
|
-
| `marginTop`, `marginBottom`... | `number` | |
|
|
87
|
-
|
|
88
|
-
### Visual Styles
|
|
89
|
-
|
|
90
|
-
| Property | Type | Notes |
|
|
91
|
-
| :--------------------------- | :------------------- | :----------------------------------------------------------------------- |
|
|
92
|
-
| `color` | `string` | **Hex String Preferred** (`'#ff0000ff'`). |
|
|
93
|
-
| `colorTop`, `colorBottom`... | `string` | Gradient-like vertex coloring in hex string. |
|
|
94
|
-
| `linearGradient` | `object` | `{ angle?: number, colors: string[], stops?: number[] }` |
|
|
95
|
-
| `radialGradient` | `object` | `{ radius?: number, colors: string[], stops?: number[], ... }` |
|
|
96
|
-
| `borderRadius` | `number \| number[]` | Single radius or [tl, tr, br, bl]. |
|
|
97
|
-
| `border` | `object` | `{ width: number, color: string }`. |
|
|
98
|
-
| `shadow` | `object` | `{ color: string, x: number, y: number, blur: number, spread: number }`. |
|
|
99
|
-
|
|
100
|
-
### Text Properties
|
|
101
|
-
|
|
102
|
-
| Property | Type | Notes |
|
|
103
|
-
| :------------- | :------------------------------- | :-------------- |
|
|
104
|
-
| `text` | `string` | Content string. |
|
|
105
|
-
| `fontSize` | `number` | |
|
|
106
|
-
| `fontFamily` | `string` | |
|
|
107
|
-
| `fontWeight` | `number \| string` | |
|
|
108
|
-
| `lineHeight` | `number` | |
|
|
109
|
-
| `textAlign` | `'left' \| 'center' \| 'right'` | |
|
|
110
|
-
| `wordWrap` | `boolean` | |
|
|
111
|
-
| `maxLines` | `number` | truncate text. |
|
|
112
|
-
| `textOverflow` | `'clip' \| 'ellipsis' \| string` | |
|
|
113
|
-
|
|
114
|
-
### Interaction & Focus
|
|
115
|
-
|
|
116
|
-
| Property | Type | Notes |
|
|
117
|
-
| :--------------- | :--------- | :----------------------------------------------------------------------- |
|
|
118
|
-
| `onFocus` | `function` | Called when element gains focus. |
|
|
119
|
-
| `onBlur` | `function` | Called when element loses focus. |
|
|
120
|
-
| `onFocusChanged` | `function` | **Preferred**. `(hasFocus: boolean) => void`. Combines focus/blur logic. |
|
|
121
|
-
| `onEnter` | `function` | Called on Enter key press. |
|
|
122
|
-
|
|
123
|
-
#### Focus Handling Best Practice
|
|
124
|
-
|
|
125
|
-
When tracking focus state (e.g., for styling), prefer `onFocusChanged` over separate `onFocus`/`onBlur` handlers.
|
|
126
|
-
|
|
127
|
-
**Preferred Pattern:**
|
|
1
|
+
# Custom TV-UI Framework: Lightning + SolidJS
|
|
128
2
|
|
|
129
|
-
|
|
130
|
-
const [focused, setFocused] = createSignal(false);
|
|
3
|
+
**System Role:** You are an expert frontend engineer working with a custom TV-UI framework called **Lightning**, built on **SolidJS**.
|
|
131
4
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
5
|
+
## 1. Core Architecture & Runtime
|
|
6
|
+
|
|
7
|
+
- **Environment:** TV app development over WebGL (not the DOM). No pointer input; interaction is directional (Up/Down/Left/Right).
|
|
8
|
+
- **Reactivity:** Uses SolidJS primitives (`createSignal`, `createEffect`, `createMemo`).
|
|
9
|
+
- **Primitives:** UI is built using custom components like `<View>`, `<Text>`, `<Row>`, `<Column>`.
|
|
10
|
+
- **Patterns:** Always use functional components and modern TypeScript/JSX. Avoid classes.
|
|
11
|
+
- **Assumption:** Always frame answers within the context of Lightning + SolidJS TV environment.
|
|
12
|
+
|
|
13
|
+
## 2. Layout & Positioning
|
|
14
|
+
|
|
15
|
+
- **Absolute by Default:** All nodes are naturally `position: absolute`.
|
|
16
|
+
- **Positioning:** Controlled explicitly via `x`, `y`, `width`, `height`.
|
|
17
|
+
- **Pinning:** Use `right` (implies `mountX: 1`) and `bottom` (implies `mountY: 1`) to pin to parent edges.
|
|
18
|
+
- **Mounting:** Default `mount` is `0, 0` (Top-Left). Value `0` to `1` determines anchor point. Avoid manual changes unless centering (`0.5`).
|
|
19
|
+
- **Dimensions:** Explicit `width` and `height` are crucial. Unspecified dimensions will inherit parent size (causing unintended overlays).
|
|
20
|
+
|
|
21
|
+
## 3. Flexbox Engine
|
|
22
|
+
|
|
23
|
+
- **Activation:** Set `display: "flex"` on containers to enable flex layout.
|
|
24
|
+
- **Padding:** Supports ONLY a single overall `padding` number. (NO `paddingLeft`, `paddingTop`, etc.).
|
|
25
|
+
- **Margins:** Supported on items (`marginTop`, `marginBottom`, `marginLeft`, `marginRight`).
|
|
26
|
+
- **Gap:** `gap`, `rowGap`, and `columnGap` are supported.
|
|
27
|
+
- **Alignment:** Strictly typed properties: `flexDirection` ('row'|'column'), `justifyContent`, `alignItems`, `alignSelf`.
|
|
28
|
+
|
|
29
|
+
## 4. Styling Strict Rules
|
|
30
|
+
|
|
31
|
+
- **Colors:** MUST use hex strings (e.g., `"#ff0000ff"`). NO named colors (e.g., `'red'`) or CSS variables.
|
|
32
|
+
- **Backgrounds:** DO NOT use `background`. Use `color` instead.
|
|
33
|
+
- **Borders/Shadows:** Use object structures (`border={{ width: 1, color: "#000000ff" }}`). NO CSS `border` or `box-shadow` strings.
|
|
34
|
+
- **Radii:** Use numeric `borderRadius` (single number or array `[tl, tr, br, bl]`).
|
|
35
|
+
- **Classes/Styles:** CSS classes and inline `style={{}}` props are NOT supported. Pass props directly to the component.
|
|
36
|
+
|
|
37
|
+
## 5. Focus & Interaction
|
|
38
|
+
|
|
39
|
+
- **Navigation:** Navigation is handled via a remote control with arrow keys. Use `onUp`, `onDown`, `onLeft`, and `onRight` to handle directional input on components.
|
|
40
|
+
- **Handling:** Prefer the `onFocusChanged={(hasFocus: boolean) => void}` prop to easily track and react to focus state (e.g. for hover styles).
|
|
41
|
+
- **Events:** `onFocus`, `onBlur`, and `onEnter` are available for direct actions.
|
|
42
|
+
- **Auto-Focus:** Exactly one item should include the `autofocus` prop (`autofocus={true}`) when a page loads.
|
|
43
|
+
- **Forwarding Focus:** Use `forwardFocus` to set focus on a child element. It can take a number (e.g., `forwardFocus={1}`) to focus a specific descendant by index.
|
|
44
|
+
- **Row/Column:** `Row` and `Column` components automatically manage selecting and setting focus on their children.
|
|
141
45
|
|
|
142
|
-
##
|
|
46
|
+
## 6. Property Reference
|
|
143
47
|
|
|
144
|
-
|
|
145
|
-
| :-------------------- | :----------------------------------------------------- |
|
|
146
|
-
| `background` | Use `color`. |
|
|
147
|
-
| Named colors | Use hex strings (`'#ff0000ff'`). |
|
|
148
|
-
| `textColor` | Use `color` on the Text node itself. |
|
|
149
|
-
| `paddingLeft`... | Use `padding` (uniform) or `margin` props on children. |
|
|
150
|
-
| `border-style` string | Use `border` object. |
|
|
151
|
-
| `display: 'grid'` | Not supported. Use nested Flexboxes. |
|
|
152
|
-
| `className` | Not supported. |
|
|
153
|
-
| `style={{ ... }}` | **Avoid**. Pass props directly to the component. |
|
|
48
|
+
### Positioning & Transformation
|
|
154
49
|
|
|
155
|
-
|
|
50
|
+
`x`, `y`, `right`, `bottom`, `width` (w), `height` (h), `minWidth`, `minHeight`, `maxWidth`, `maxHeight`, `mount`, `mountX`, `mountY`, `pivot`, `pivotX`, `pivotY`, `rotation`, `scale`, `scaleX`, `scaleY`, `alpha`, `zIndex`, `zIndexLocked`
|
|
156
51
|
|
|
157
|
-
###
|
|
52
|
+
### Container Flexbox
|
|
53
|
+
|
|
54
|
+
`display: "flex"`, `flexDirection`, `flexWrap`, `justifyContent`, `alignItems`, `gap`, `rowGap`, `columnGap`, `padding`
|
|
55
|
+
|
|
56
|
+
### Item Flexbox
|
|
57
|
+
|
|
58
|
+
`flexGrow`, `flexItem`, `alignSelf`, `marginTop`, `marginBottom`, `marginLeft`, `marginRight`
|
|
59
|
+
|
|
60
|
+
### Visual & Text
|
|
61
|
+
|
|
62
|
+
`color`, `colorTop`, `colorBottom`, `linearGradient`, `radialGradient`, `borderRadius`, `border`, `shadow`, `text`, `fontSize`, `fontFamily`, `fontWeight`, `lineHeight`, `textAlign`, `wordWrap`, `maxLines`, `textOverflow`
|
|
63
|
+
|
|
64
|
+
## 7. DO NOT USE 🚫
|
|
65
|
+
|
|
66
|
+
- Standard DOM Elements (`<div>`, `<span>`, etc.)
|
|
67
|
+
- CSS Class Names (`class`, `className`)
|
|
68
|
+
- The `style={{}}` Prop (Use native node props instead)
|
|
69
|
+
- `display: 'grid'`
|
|
70
|
+
- String literal colors without hex (e.g. `'red'`, `'#F00'`) - Use full hex codes like `'#ff0000ff'`
|
|
71
|
+
- Directional paddings (e.g., `paddingLeft`)
|
|
72
|
+
|
|
73
|
+
## 8. Code Examples
|
|
74
|
+
|
|
75
|
+
**❌ Incorrect:**
|
|
158
76
|
|
|
159
77
|
```tsx
|
|
160
|
-
//
|
|
161
|
-
// 2. Using named colors
|
|
162
|
-
// 3. Using style object
|
|
78
|
+
// Missing dimensions, invalid colors, using styles, directional padding
|
|
163
79
|
<View
|
|
164
80
|
style={{
|
|
165
81
|
backgroundColor: 'red',
|
|
@@ -172,20 +88,21 @@ return (
|
|
|
172
88
|
</View>
|
|
173
89
|
```
|
|
174
90
|
|
|
175
|
-
|
|
91
|
+
**✅ Correct:**
|
|
176
92
|
|
|
177
93
|
```tsx
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
//
|
|
94
|
+
const [focused, setFocused] = createSignal(false);
|
|
95
|
+
|
|
96
|
+
// Uses direct props, hex colors, clear dimensions, and focus tracking
|
|
181
97
|
<View
|
|
182
|
-
width={400}
|
|
183
|
-
height={200}
|
|
184
|
-
color=
|
|
185
|
-
padding={20}
|
|
186
|
-
borderRadius={10}
|
|
187
|
-
display="flex"
|
|
98
|
+
width={400}
|
|
99
|
+
height={200}
|
|
100
|
+
color={focused() ? '#ffff00ff' : '#ff0000ff'}
|
|
101
|
+
padding={20}
|
|
102
|
+
borderRadius={10}
|
|
103
|
+
display="flex"
|
|
104
|
+
onFocusChanged={setFocused}
|
|
188
105
|
>
|
|
189
106
|
<Text color="#ffffffff">Hello</Text>
|
|
190
|
-
</View
|
|
107
|
+
</View>;
|
|
191
108
|
```
|
package/package.json
CHANGED
package/src/core/elementNode.ts
CHANGED
|
@@ -61,6 +61,8 @@ import {
|
|
|
61
61
|
} from './focusManager.js';
|
|
62
62
|
import simpleAnimation, { SimpleAnimationSettings } from './animation.js';
|
|
63
63
|
|
|
64
|
+
let nextActiveElement: ElementNode | null = null;
|
|
65
|
+
let focusQueued: boolean = false;
|
|
64
66
|
let layoutRunQueued = false;
|
|
65
67
|
const layoutQueue = new Set<ElementNode>();
|
|
66
68
|
|
|
@@ -835,7 +837,10 @@ export class ElementNode extends Object {
|
|
|
835
837
|
const animationSettings =
|
|
836
838
|
this.transition === true || this.transition[name] === true
|
|
837
839
|
? undefined
|
|
838
|
-
:
|
|
840
|
+
: this.transition[name] ||
|
|
841
|
+
(this.transition[getPropertyAlias(name)] as
|
|
842
|
+
| undefined
|
|
843
|
+
| AnimationSettings);
|
|
839
844
|
|
|
840
845
|
if (Config.simpleAnimationsEnabled) {
|
|
841
846
|
simpleAnimation.add(
|
|
@@ -957,7 +962,15 @@ export class ElementNode extends Object {
|
|
|
957
962
|
}
|
|
958
963
|
}
|
|
959
964
|
// Delay setting focus so children can render (useful for Row + Column)
|
|
960
|
-
|
|
965
|
+
nextActiveElement = this;
|
|
966
|
+
if (focusQueued === false) {
|
|
967
|
+
focusQueued = true;
|
|
968
|
+
queueMicrotask(() => {
|
|
969
|
+
if (nextActiveElement) setActiveElement(nextActiveElement);
|
|
970
|
+
nextActiveElement = null;
|
|
971
|
+
focusQueued = false;
|
|
972
|
+
});
|
|
973
|
+
}
|
|
961
974
|
} else {
|
|
962
975
|
this._autofocus = true;
|
|
963
976
|
}
|
package/src/core/focusManager.ts
CHANGED
|
@@ -173,11 +173,13 @@ const propagateKeyPress = (
|
|
|
173
173
|
}
|
|
174
174
|
lastGlobalKeyPressTime = currentTime;
|
|
175
175
|
}
|
|
176
|
-
let finalFocusElm: ElementNode | undefined;
|
|
177
|
-
let handlerAvailable: ElementNode | undefined;
|
|
178
176
|
const numItems = focusPath.length;
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
if (numItems === 0) return false;
|
|
178
|
+
|
|
179
|
+
let handlerAvailable: ElementNode | undefined;
|
|
180
|
+
const finalFocusElm = focusPath[0]!;
|
|
181
|
+
const keyBase = mappedEvent || (e.key as string);
|
|
182
|
+
const captureEvent = `onCapture${keyBase}${isUp ? 'Release' : ''}`;
|
|
181
183
|
const captureKey = isUp ? 'onCaptureKeyRelease' : 'onCaptureKey';
|
|
182
184
|
|
|
183
185
|
for (let i = numItems - 1; i >= 0; i--) {
|
|
@@ -205,12 +207,10 @@ const propagateKeyPress = (
|
|
|
205
207
|
}
|
|
206
208
|
|
|
207
209
|
let eventHandlerKey: string | undefined;
|
|
208
|
-
let releaseEventHandlerKey: string | undefined;
|
|
209
210
|
let fallbackHandlerKey: 'onKeyHold' | 'onKeyPress' | undefined;
|
|
210
211
|
|
|
211
212
|
if (mappedEvent) {
|
|
212
|
-
eventHandlerKey = `on${mappedEvent}`;
|
|
213
|
-
releaseEventHandlerKey = `on${mappedEvent}Release`;
|
|
213
|
+
eventHandlerKey = isUp ? `on${mappedEvent}Release` : `on${mappedEvent}`;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
if (!isUp) {
|
|
@@ -219,9 +219,6 @@ const propagateKeyPress = (
|
|
|
219
219
|
|
|
220
220
|
for (let i = 0; i < numItems; i++) {
|
|
221
221
|
const elm = focusPath[i]!;
|
|
222
|
-
if (!finalFocusElm) {
|
|
223
|
-
finalFocusElm = elm;
|
|
224
|
-
}
|
|
225
222
|
|
|
226
223
|
// Check throttle for bubbling phase
|
|
227
224
|
if (elm.throttleInput) {
|
|
@@ -236,21 +233,13 @@ const propagateKeyPress = (
|
|
|
236
233
|
|
|
237
234
|
let handled = false;
|
|
238
235
|
|
|
239
|
-
|
|
240
|
-
if (isUp && releaseEventHandlerKey) {
|
|
241
|
-
const eventHandler = elm[releaseEventHandlerKey];
|
|
242
|
-
if (isFunction(eventHandler)) {
|
|
243
|
-
handlerAvailable = elm;
|
|
244
|
-
if (eventHandler.call(elm, e, elm, finalFocusElm) === true)
|
|
245
|
-
handled = true;
|
|
246
|
-
}
|
|
247
|
-
} else if (!isUp && eventHandlerKey) {
|
|
248
|
-
// Check for the regular event handler if isUp is false and the key is defined
|
|
236
|
+
if (eventHandlerKey) {
|
|
249
237
|
const eventHandler = elm[eventHandlerKey];
|
|
250
238
|
if (isFunction(eventHandler)) {
|
|
251
239
|
handlerAvailable = elm;
|
|
252
|
-
if (eventHandler.call(elm, e, elm, finalFocusElm) === true)
|
|
240
|
+
if (eventHandler.call(elm, e, elm, finalFocusElm) === true) {
|
|
253
241
|
handled = true;
|
|
242
|
+
}
|
|
254
243
|
}
|
|
255
244
|
}
|
|
256
245
|
|
|
@@ -261,8 +250,9 @@ const propagateKeyPress = (
|
|
|
261
250
|
handlerAvailable = elm;
|
|
262
251
|
if (
|
|
263
252
|
fallbackHandler.call(elm, e, mappedEvent, elm, finalFocusElm) === true
|
|
264
|
-
)
|
|
253
|
+
) {
|
|
265
254
|
handled = true;
|
|
255
|
+
}
|
|
266
256
|
}
|
|
267
257
|
}
|
|
268
258
|
|
package/src/primitives/Grid.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { For, createSignal, createMemo, createEffect, JSX } from "solid-js";
|
|
2
|
-
import { type NodeProps, ElementNode, NewOmit } from "@lightningtv/solid";
|
|
1
|
+
import { For, createSignal, createMemo, createEffect, JSX, untrack, Index } from "solid-js";
|
|
2
|
+
import { type NodeProps, ElementNode, NewOmit, hasFocus } from "@lightningtv/solid";
|
|
3
3
|
import { chainRefs } from "./utils/chainFunctions.js";
|
|
4
4
|
|
|
5
5
|
export interface GridItemProps<T> {
|
|
@@ -20,6 +20,7 @@ export interface GridProps<T> extends NewOmit<NodeProps, 'children'> {
|
|
|
20
20
|
columns?: number;
|
|
21
21
|
looping?: boolean;
|
|
22
22
|
scroll?: "auto" | "none";
|
|
23
|
+
selected?: number;
|
|
23
24
|
onSelectedChanged?: (index: number, grid: ElementNode, elm?: ElementNode) => void;
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -28,16 +29,25 @@ export function Grid<T>(props: GridProps<T>): JSX.Element {
|
|
|
28
29
|
const [focusedIndex, setFocusedIndex] = createSignal(0);
|
|
29
30
|
const baseColumns = 4;
|
|
30
31
|
|
|
32
|
+
createEffect(() => {
|
|
33
|
+
const currentIndex = untrack(focusedIndex);
|
|
34
|
+
if (props.selected === currentIndex) return;
|
|
35
|
+
if (props.selected !== undefined && props.items?.length > props.selected) {
|
|
36
|
+
moveFocus(props.selected! - currentIndex);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
31
40
|
const itemWidth = () => props.itemWidth ?? 300
|
|
32
41
|
const itemHeight = () => props.itemHeight ?? 300
|
|
33
42
|
|
|
34
43
|
const columns = createMemo(() => props.columns || baseColumns);
|
|
35
44
|
const totalWidth = createMemo(() => itemWidth() + (props.itemOffset ?? 0));
|
|
36
45
|
const totalHeight = createMemo(() => itemHeight() + (props.itemOffset ?? 0));
|
|
46
|
+
const rows = createMemo(() => Math.ceil(props.items.length / columns()));
|
|
37
47
|
|
|
38
48
|
function focus() {
|
|
39
49
|
const focusedElm = gridRef.children[focusedIndex()];
|
|
40
|
-
if (focusedElm instanceof ElementNode && !focusedElm
|
|
50
|
+
if (focusedElm instanceof ElementNode && !hasFocus(focusedElm)) {
|
|
41
51
|
focusedElm.setFocus();
|
|
42
52
|
props.onSelectedChanged?.call(gridRef, focusedIndex(), gridRef, focusedElm);
|
|
43
53
|
return true;
|
|
@@ -99,27 +109,27 @@ export function Grid<T>(props: GridProps<T>): JSX.Element {
|
|
|
99
109
|
<view
|
|
100
110
|
{...props}
|
|
101
111
|
ref={chainRefs(el => gridRef = el, props.ref)}
|
|
102
|
-
transition={{ y: true }}
|
|
112
|
+
transition={/* @once */ { y: true }}
|
|
113
|
+
height={totalHeight() * rows()}
|
|
103
114
|
onUp={() => moveFocus(-columns())}
|
|
104
115
|
onDown={() => moveFocus(columns())}
|
|
105
116
|
onLeft={() => handleHorizontalFocus(-1)}
|
|
106
117
|
onRight={() => handleHorizontalFocus(1)}
|
|
107
118
|
onFocus={() => handleHorizontalFocus(0)}
|
|
108
|
-
strictBounds={false}
|
|
109
119
|
y={scrollY()}
|
|
110
120
|
>
|
|
111
|
-
<
|
|
121
|
+
<Index each={props.items}>
|
|
112
122
|
{(item, index) => (
|
|
113
123
|
<props.children
|
|
114
|
-
item={item}
|
|
115
|
-
index={index
|
|
124
|
+
item={item()}
|
|
125
|
+
index={index}
|
|
116
126
|
width={itemWidth()}
|
|
117
127
|
height={itemHeight()}
|
|
118
|
-
x={(index
|
|
119
|
-
y={Math.floor(index
|
|
128
|
+
x={(index % columns()) * totalWidth()}
|
|
129
|
+
y={Math.floor(index / columns()) * totalHeight()}
|
|
120
130
|
/>
|
|
121
131
|
)}
|
|
122
|
-
</
|
|
132
|
+
</Index>
|
|
123
133
|
</view>
|
|
124
134
|
);
|
|
125
135
|
};
|