canvasengine 2.0.0-beta.45 → 2.0.0-beta.46
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/components/Container.d.ts +86 -0
- package/dist/components/Container.d.ts.map +1 -0
- package/dist/components/DOMContainer.d.ts +98 -0
- package/dist/components/DOMContainer.d.ts.map +1 -0
- package/dist/components/DOMElement.d.ts +16 -5
- package/dist/components/DOMElement.d.ts.map +1 -1
- package/dist/components/DOMSprite.d.ts +108 -0
- package/dist/components/DOMSprite.d.ts.map +1 -0
- package/dist/components/DisplayObject.d.ts +94 -0
- package/dist/components/DisplayObject.d.ts.map +1 -0
- package/dist/components/FocusContainer.d.ts +129 -0
- package/dist/components/FocusContainer.d.ts.map +1 -0
- package/dist/components/Mesh.d.ts +208 -0
- package/dist/components/Mesh.d.ts.map +1 -0
- package/dist/components/Sprite.d.ts +242 -0
- package/dist/components/Sprite.d.ts.map +1 -0
- package/dist/components/Viewport.d.ts +121 -0
- package/dist/components/Viewport.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/directives/Controls.d.ts +4 -4
- package/dist/directives/Controls.d.ts.map +1 -1
- package/dist/directives/ControlsBase.d.ts +1 -0
- package/dist/directives/ControlsBase.d.ts.map +1 -1
- package/dist/directives/FocusNavigation.d.ts +4 -22
- package/dist/directives/FocusNavigation.d.ts.map +1 -1
- package/dist/directives/KeyboardControls.d.ts +1 -0
- package/dist/directives/KeyboardControls.d.ts.map +1 -1
- package/dist/directives/Scheduler.d.ts.map +1 -1
- package/dist/directives/Shake.d.ts +1 -0
- package/dist/directives/Shake.d.ts.map +1 -1
- package/dist/engine/FocusManager.d.ts +10 -9
- package/dist/engine/FocusManager.d.ts.map +1 -1
- package/dist/engine/bootstrap.d.ts +1 -0
- package/dist/engine/bootstrap.d.ts.map +1 -1
- package/dist/engine/directive.d.ts +1 -1
- package/dist/engine/directive.d.ts.map +1 -1
- package/dist/engine/reactive.d.ts.map +1 -1
- package/dist/hooks/useFocus.d.ts.map +1 -1
- package/dist/index-DaGekQUW.js +2218 -0
- package/dist/index-DaGekQUW.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.js +3 -3
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +11868 -88
- package/dist/index.js.map +1 -1
- package/dist/utils/tabindex.d.ts +16 -0
- package/dist/utils/tabindex.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/DOMContainer.ts +186 -1
- package/src/components/DOMElement.ts +164 -37
- package/src/components/DOMSprite.ts +759 -0
- package/src/components/DisplayObject.ts +33 -7
- package/src/components/FocusContainer.ts +22 -26
- package/src/components/Sprite.ts +12 -3
- package/src/components/Text.ts +1 -1
- package/src/components/Viewport.ts +5 -5
- package/src/components/index.ts +2 -1
- package/src/directives/Controls.ts +5 -5
- package/src/directives/ControlsBase.ts +1 -0
- package/src/directives/FocusNavigation.ts +8 -146
- package/src/directives/KeyboardControls.ts +11 -2
- package/src/directives/Scheduler.ts +12 -4
- package/src/directives/Shake.ts +9 -6
- package/src/engine/FocusManager.ts +44 -29
- package/src/engine/bootstrap.ts +5 -2
- package/src/engine/directive.ts +2 -2
- package/src/engine/reactive.ts +84 -12
- package/src/hooks/useFocus.ts +2 -5
- package/src/index.ts +2 -1
- package/src/types/pixi-cull.d.ts +7 -0
- package/src/utils/tabindex.ts +70 -0
- package/testing/index.ts +31 -3
- package/tsconfig.json +3 -2
- package/dist/DebugRenderer-CSxse9YI.js +0 -172
- package/dist/DebugRenderer-CSxse9YI.js.map +0 -1
- package/dist/index-DH2ZMhYm.js +0 -13276
- package/dist/index-DH2ZMhYm.js.map +0 -1
package/src/directives/Shake.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Container, Point } from 'pixi.js';
|
|
2
2
|
import { Directive, registerDirective } from '../engine/directive';
|
|
3
3
|
import { Element } from '../engine/reactive';
|
|
4
|
-
import { effect } from '@signe/reactive';
|
|
4
|
+
import { effect, isSignal } from '@signe/reactive';
|
|
5
5
|
import { on, isTrigger, Trigger } from '../engine/trigger';
|
|
6
6
|
import { useProps } from '../hooks/useProps';
|
|
7
7
|
import { SignalOrPrimitive } from '../components/types';
|
|
@@ -128,6 +128,10 @@ export class Shake extends Directive {
|
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
private resolveSignalValue<T>(value: SignalOrPrimitive<T>): T {
|
|
132
|
+
return (isSignal(value as any) ? (value as any)() : value) as T;
|
|
133
|
+
}
|
|
134
|
+
|
|
131
135
|
/**
|
|
132
136
|
* Performs the shake animation using animatedSignal
|
|
133
137
|
* @param data - Optional data passed from the trigger that can override default options
|
|
@@ -139,10 +143,10 @@ export class Shake extends Directive {
|
|
|
139
143
|
const shakeProps = this.shakeProps;
|
|
140
144
|
|
|
141
145
|
// Use data from trigger to override defaults if provided
|
|
142
|
-
const intensity = data?.intensity ?? shakeProps.intensity
|
|
143
|
-
const duration = data?.duration ?? shakeProps.duration
|
|
144
|
-
const frequency = data?.frequency ?? shakeProps.frequency
|
|
145
|
-
const direction = data?.direction ?? shakeProps.direction
|
|
146
|
+
const intensity = data?.intensity ?? this.resolveSignalValue(shakeProps.intensity);
|
|
147
|
+
const duration = data?.duration ?? this.resolveSignalValue(shakeProps.duration);
|
|
148
|
+
const frequency = data?.frequency ?? this.resolveSignalValue(shakeProps.frequency);
|
|
149
|
+
const direction = data?.direction ?? this.resolveSignalValue(shakeProps.direction);
|
|
146
150
|
|
|
147
151
|
// Stop any existing animation and clean up
|
|
148
152
|
if (this.positionEffect) {
|
|
@@ -292,4 +296,3 @@ export class Shake extends Directive {
|
|
|
292
296
|
}
|
|
293
297
|
|
|
294
298
|
registerDirective('shake', Shake);
|
|
295
|
-
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isSignal, signal, Signal } from "@signe/reactive";
|
|
1
|
+
import { isSignal, signal, Signal, WritableSignal, WritableObjectSignal } from "@signe/reactive";
|
|
2
2
|
import { Element, isElementFrozen } from "./reactive";
|
|
3
3
|
import { CanvasViewport } from "../components/Viewport";
|
|
4
4
|
import { SignalOrPrimitive } from "../components/types";
|
|
@@ -21,19 +21,20 @@ export interface ScrollOptions {
|
|
|
21
21
|
/**
|
|
22
22
|
* Data structure for a focus container
|
|
23
23
|
*/
|
|
24
|
+
type WritableElementSignal = WritableSignal<Element | null> | WritableObjectSignal<Element | null>;
|
|
25
|
+
|
|
24
26
|
interface FocusContainerData {
|
|
25
27
|
id: string;
|
|
26
|
-
element?: Element
|
|
27
|
-
focusables: Map<number, Element
|
|
28
|
-
currentIndex:
|
|
29
|
-
focusedElement:
|
|
28
|
+
element?: Element<any>;
|
|
29
|
+
focusables: Map<number, Element<any>>;
|
|
30
|
+
currentIndex: WritableSignal<number | null>;
|
|
31
|
+
focusedElement: WritableElementSignal;
|
|
30
32
|
onFocusChange?: (index: number, element: Element | null) => void;
|
|
31
33
|
autoScroll?: boolean | ScrollOptions;
|
|
32
34
|
viewport?: CanvasViewport;
|
|
33
|
-
|
|
34
|
-
lastNavigateTime?: number;
|
|
35
|
-
tabindex?: SignalOrPrimitive<number>;
|
|
35
|
+
tabindex?: SignalOrPrimitive<number> | null;
|
|
36
36
|
tabindexSubscription?: any;
|
|
37
|
+
pendingIndex?: number;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -89,7 +90,7 @@ export class FocusManager {
|
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
setTabindex(id: string, tabindex
|
|
93
|
+
setTabindex(id: string, tabindex?: SignalOrPrimitive<number> | null): void {
|
|
93
94
|
const container = this.containers.get(id);
|
|
94
95
|
if (!container) return;
|
|
95
96
|
|
|
@@ -98,10 +99,20 @@ export class FocusManager {
|
|
|
98
99
|
container.tabindexSubscription.unsubscribe();
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
if (tabindex === undefined || tabindex === null) {
|
|
103
|
+
container.tabindex = undefined;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
101
107
|
container.tabindex = tabindex;
|
|
102
108
|
|
|
109
|
+
const currentTabindex = isSignal(tabindex) ? (tabindex as Signal<number>)() : tabindex;
|
|
110
|
+
if (typeof currentTabindex === "number") {
|
|
111
|
+
this.setIndex(id, currentTabindex);
|
|
112
|
+
}
|
|
113
|
+
|
|
103
114
|
if (isSignal(tabindex)) {
|
|
104
|
-
container.tabindexSubscription = (tabindex as Signal<number>).observable.subscribe((value: any) => {
|
|
115
|
+
container.tabindexSubscription = ((tabindex as Signal<number>).observable as any).subscribe((value: any) => {
|
|
105
116
|
if (value !== null && value !== container.currentIndex()) {
|
|
106
117
|
this.setIndex(id, value);
|
|
107
118
|
}
|
|
@@ -136,8 +147,9 @@ export class FocusManager {
|
|
|
136
147
|
|
|
137
148
|
// If this is the index we are supposed to be at, set it now
|
|
138
149
|
const currentTabindex = isSignal(container.tabindex) ? (container.tabindex as Signal<number>)() : container.tabindex;
|
|
139
|
-
if (currentTabindex === index && container.currentIndex() === null) {
|
|
140
|
-
|
|
150
|
+
if (container.pendingIndex === index || (currentTabindex === index && container.currentIndex() === null)) {
|
|
151
|
+
container.pendingIndex = undefined;
|
|
152
|
+
this.applyFocus(container, containerId, index, element);
|
|
141
153
|
}
|
|
142
154
|
}
|
|
143
155
|
|
|
@@ -170,16 +182,6 @@ export class FocusManager {
|
|
|
170
182
|
return;
|
|
171
183
|
}
|
|
172
184
|
|
|
173
|
-
// Handle throttling
|
|
174
|
-
if (container.throttle) {
|
|
175
|
-
const now = Date.now();
|
|
176
|
-
const lastTime = container.lastNavigateTime || 0;
|
|
177
|
-
if (now - lastTime < container.throttle) {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
container.lastNavigateTime = now;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
185
|
const currentIndex = container.currentIndex();
|
|
184
186
|
const focusableIndices = Array.from(container.focusables.keys()).sort((a, b) => a - b);
|
|
185
187
|
|
|
@@ -211,7 +213,7 @@ export class FocusManager {
|
|
|
211
213
|
|
|
212
214
|
if (newIndex !== null) {
|
|
213
215
|
const tabindex = container.tabindex;
|
|
214
|
-
if (isSignal(tabindex)) {
|
|
216
|
+
if (isSignal(tabindex) && typeof (tabindex as any).set === "function") {
|
|
215
217
|
(tabindex as any).set(newIndex);
|
|
216
218
|
} else {
|
|
217
219
|
this.setIndex(containerId, newIndex);
|
|
@@ -231,16 +233,24 @@ export class FocusManager {
|
|
|
231
233
|
|
|
232
234
|
const element = container.focusables.get(index);
|
|
233
235
|
if (!element) {
|
|
234
|
-
|
|
236
|
+
container.pendingIndex = index;
|
|
235
237
|
return;
|
|
236
238
|
}
|
|
239
|
+
this.applyFocus(container, containerId, index, element);
|
|
240
|
+
}
|
|
237
241
|
|
|
242
|
+
private applyFocus(
|
|
243
|
+
container: FocusContainerData,
|
|
244
|
+
containerId: string,
|
|
245
|
+
index: number,
|
|
246
|
+
element: Element
|
|
247
|
+
): void {
|
|
238
248
|
container.currentIndex.set(index);
|
|
239
249
|
container.focusedElement.set(element);
|
|
240
250
|
|
|
241
251
|
// Sync back to tabindex signal if it exists
|
|
242
252
|
const tabindex = container.tabindex;
|
|
243
|
-
if (isSignal(tabindex) && (tabindex as any)() !== index) {
|
|
253
|
+
if (isSignal(tabindex) && (tabindex as any)() !== index && typeof (tabindex as any).set === "function") {
|
|
244
254
|
(tabindex as any).set(index);
|
|
245
255
|
}
|
|
246
256
|
|
|
@@ -308,7 +318,7 @@ export class FocusManager {
|
|
|
308
318
|
*/
|
|
309
319
|
getFocusedElementSignal(containerId: string): Signal<Element | null> | null {
|
|
310
320
|
const container = this.containers.get(containerId);
|
|
311
|
-
return container ? container.focusedElement : null;
|
|
321
|
+
return container ? (container.focusedElement as unknown as Signal<Element | null>) : null;
|
|
312
322
|
}
|
|
313
323
|
|
|
314
324
|
/**
|
|
@@ -345,10 +355,16 @@ export class FocusManager {
|
|
|
345
355
|
}
|
|
346
356
|
|
|
347
357
|
// Get local bounds
|
|
348
|
-
const localBounds = instance.getLocalBounds();
|
|
358
|
+
const localBounds = instance.getLocalBounds?.();
|
|
359
|
+
if (!localBounds) {
|
|
360
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
361
|
+
}
|
|
349
362
|
|
|
350
363
|
// Get global position
|
|
351
|
-
const globalPos = instance.getGlobalPosition();
|
|
364
|
+
const globalPos = instance.getGlobalPosition?.();
|
|
365
|
+
if (!globalPos) {
|
|
366
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
367
|
+
}
|
|
352
368
|
|
|
353
369
|
return {
|
|
354
370
|
x: globalPos.x,
|
|
@@ -492,4 +508,3 @@ export class FocusManager {
|
|
|
492
508
|
|
|
493
509
|
// Export singleton instance
|
|
494
510
|
export const focusManager = FocusManager.getInstance();
|
|
495
|
-
|
package/src/engine/bootstrap.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import '@pixi/layout';
|
|
2
1
|
import { Application, ApplicationOptions } from "pixi.js";
|
|
3
2
|
import { ComponentFunction, h } from "./signal";
|
|
4
3
|
import { useProps } from '../hooks/useProps';
|
|
@@ -31,6 +30,7 @@ export interface BootstrapOptions extends ApplicationOptions {
|
|
|
31
30
|
[name: string]: any; // ComponentClass
|
|
32
31
|
};
|
|
33
32
|
autoRegister?: boolean; // true by default if components is not provided
|
|
33
|
+
enableLayout?: boolean; // true by default
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
@@ -63,7 +63,10 @@ export interface BootstrapOptions extends ApplicationOptions {
|
|
|
63
63
|
*/
|
|
64
64
|
export const bootstrapCanvas = async (rootElement: HTMLElement | null, canvas: ComponentFunction<any>, options?: BootstrapOptions) => {
|
|
65
65
|
// Extract component registration options
|
|
66
|
-
const { components, autoRegister, ...appOptions } = options ?? {};
|
|
66
|
+
const { components, autoRegister, enableLayout, ...appOptions } = options ?? {};
|
|
67
|
+
if (enableLayout !== false) {
|
|
68
|
+
await import('@pixi/layout');
|
|
69
|
+
}
|
|
67
70
|
|
|
68
71
|
// Handle component registration
|
|
69
72
|
if (components) {
|
package/src/engine/directive.ts
CHANGED
|
@@ -13,11 +13,11 @@ export function registerDirective(name: string, directive: any) {
|
|
|
13
13
|
directives[name] = directive
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function applyDirective(element: Element
|
|
16
|
+
export function applyDirective(element: Element<any>, directiveName: string) {
|
|
17
17
|
if (!directives[directiveName]) {
|
|
18
18
|
return null
|
|
19
19
|
}
|
|
20
20
|
const directive = new directives[directiveName]()
|
|
21
21
|
directive.onInit?.(element)
|
|
22
22
|
return directive
|
|
23
|
-
}
|
|
23
|
+
}
|
package/src/engine/reactive.ts
CHANGED
|
@@ -81,6 +81,72 @@ export const isPrimitive = (value) => {
|
|
|
81
81
|
);
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
+
const DOM_ROUTING_MAP: Record<string, string> = {
|
|
85
|
+
Sprite: "DOMSprite",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const DOM_ALLOWED_TAGS = new Set(["DOMContainer", "DOMElement", "DOMSprite"]);
|
|
89
|
+
const DOM_UNSUPPORTED_TAGS = new Set([
|
|
90
|
+
"Canvas",
|
|
91
|
+
"Container",
|
|
92
|
+
"Graphics",
|
|
93
|
+
"Rect",
|
|
94
|
+
"Circle",
|
|
95
|
+
"Ellipse",
|
|
96
|
+
"Triangle",
|
|
97
|
+
"Svg",
|
|
98
|
+
"Mesh",
|
|
99
|
+
"Scene",
|
|
100
|
+
"ParticlesEmitter",
|
|
101
|
+
"Sprite",
|
|
102
|
+
"Video",
|
|
103
|
+
"Text",
|
|
104
|
+
"TilingSprite",
|
|
105
|
+
"Viewport",
|
|
106
|
+
"NineSliceSprite",
|
|
107
|
+
"Button",
|
|
108
|
+
"Joystick",
|
|
109
|
+
"FocusContainer",
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const hasDomAncestor = (element: Element | null): boolean => {
|
|
113
|
+
let current = element;
|
|
114
|
+
while (current) {
|
|
115
|
+
if (current.tag === "DOMContainer" || current.tag === "DOMElement") {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
current = current.parent;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const cleanupElementForRouting = (element: Element) => {
|
|
124
|
+
element.propSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
125
|
+
element.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
126
|
+
element.effectUnmounts?.forEach((fn) => fn?.());
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const routeDomComponent = (parent: Element, child: Element): Element => {
|
|
130
|
+
if (!hasDomAncestor(parent)) {
|
|
131
|
+
return child;
|
|
132
|
+
}
|
|
133
|
+
if (DOM_ALLOWED_TAGS.has(child.tag)) {
|
|
134
|
+
return child;
|
|
135
|
+
}
|
|
136
|
+
const routedTag = DOM_ROUTING_MAP[child.tag];
|
|
137
|
+
if (routedTag) {
|
|
138
|
+
cleanupElementForRouting(child);
|
|
139
|
+
const routedProps = child.propObservables ?? child.props;
|
|
140
|
+
return createComponent(routedTag, routedProps);
|
|
141
|
+
}
|
|
142
|
+
if (DOM_UNSUPPORTED_TAGS.has(child.tag)) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Component ${child.tag} is not implemented for DOMContainer context yet. Only Sprite is supported.`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return child;
|
|
148
|
+
};
|
|
149
|
+
|
|
84
150
|
export function registerComponent(name, component) {
|
|
85
151
|
components[name] = component;
|
|
86
152
|
}
|
|
@@ -609,8 +675,9 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
609
675
|
// Handle observable component recursively
|
|
610
676
|
await createElement(parent, c);
|
|
611
677
|
} else if (isElement(c)) {
|
|
612
|
-
|
|
613
|
-
|
|
678
|
+
const routed = routeDomComponent(parent, c);
|
|
679
|
+
onMount(parent, routed, index + 1);
|
|
680
|
+
propagateContext(routed);
|
|
614
681
|
}
|
|
615
682
|
});
|
|
616
683
|
return;
|
|
@@ -621,8 +688,9 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
621
688
|
// Handle observable component recursively
|
|
622
689
|
await createElement(parent, component);
|
|
623
690
|
} else if (isElement(component)) {
|
|
624
|
-
|
|
625
|
-
|
|
691
|
+
const routed = routeDomComponent(parent, component);
|
|
692
|
+
onMount(parent, routed);
|
|
693
|
+
propagateContext(routed);
|
|
626
694
|
}
|
|
627
695
|
} else {
|
|
628
696
|
component.forEach(async (comp) => {
|
|
@@ -630,16 +698,18 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
630
698
|
// Handle observable component recursively
|
|
631
699
|
await createElement(parent, comp);
|
|
632
700
|
} else if (isElement(comp)) {
|
|
633
|
-
|
|
634
|
-
|
|
701
|
+
const routed = routeDomComponent(parent, comp);
|
|
702
|
+
onMount(parent, routed);
|
|
703
|
+
propagateContext(routed);
|
|
635
704
|
}
|
|
636
705
|
});
|
|
637
706
|
}
|
|
638
707
|
});
|
|
639
708
|
} else if (isElement(value)) {
|
|
640
709
|
// Handle direct Element emission
|
|
641
|
-
|
|
642
|
-
|
|
710
|
+
const routed = routeDomComponent(parent, value);
|
|
711
|
+
onMount(parent, routed);
|
|
712
|
+
propagateContext(routed);
|
|
643
713
|
} else if (Array.isArray(value)) {
|
|
644
714
|
// Handle array of elements (which can also be observables)
|
|
645
715
|
value.forEach(async (element) => {
|
|
@@ -647,8 +717,9 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
647
717
|
// Handle observable element recursively
|
|
648
718
|
await createElement(parent, element);
|
|
649
719
|
} else if (isElement(element)) {
|
|
650
|
-
|
|
651
|
-
|
|
720
|
+
const routed = routeDomComponent(parent, element);
|
|
721
|
+
onMount(parent, routed);
|
|
722
|
+
propagateContext(routed);
|
|
652
723
|
}
|
|
653
724
|
});
|
|
654
725
|
}
|
|
@@ -659,8 +730,9 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
659
730
|
// Store subscription for cleanup
|
|
660
731
|
parent.effectSubscriptions.push(subscription);
|
|
661
732
|
} else if (isElement(child)) {
|
|
662
|
-
|
|
663
|
-
|
|
733
|
+
const routed = routeDomComponent(parent, child);
|
|
734
|
+
onMount(parent, routed);
|
|
735
|
+
await propagateContext(routed);
|
|
664
736
|
}
|
|
665
737
|
}
|
|
666
738
|
|
package/src/hooks/useFocus.ts
CHANGED
|
@@ -78,7 +78,7 @@ export function useFocusChange(
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// Set up reactive effect
|
|
81
|
-
const
|
|
81
|
+
const effectResult = effect(() => {
|
|
82
82
|
const index = indexSignal();
|
|
83
83
|
const element = elementSignal();
|
|
84
84
|
callback(index, element);
|
|
@@ -86,9 +86,6 @@ export function useFocusChange(
|
|
|
86
86
|
|
|
87
87
|
// Return cleanup function
|
|
88
88
|
return () => {
|
|
89
|
-
|
|
90
|
-
subscription.unsubscribe();
|
|
91
|
-
}
|
|
89
|
+
effectResult.subscription?.unsubscribe();
|
|
92
90
|
};
|
|
93
91
|
}
|
|
94
|
-
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,8 @@ export { useProps, useDefineProps } from './hooks/useProps'
|
|
|
14
14
|
export { useFocusIndex, useFocusedElement, useFocusChange } from './hooks/useFocus'
|
|
15
15
|
export * from './utils/Ease'
|
|
16
16
|
export * from './utils/RadialGradient'
|
|
17
|
+
export * from './utils/tabindex'
|
|
17
18
|
export * from './components/DisplayObject'
|
|
18
19
|
export { isObservable } from 'rxjs'
|
|
19
20
|
export * as Utils from './engine/utils'
|
|
20
|
-
export * as Howl from 'howler'
|
|
21
|
+
export * as Howl from 'howler'
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { WritableSignal } from "@signe/reactive";
|
|
2
|
+
|
|
3
|
+
export type TabindexBoundaryMode = "wrap" | "clamp" | "none";
|
|
4
|
+
|
|
5
|
+
export type TabindexBounds =
|
|
6
|
+
| { count: () => number; min?: number }
|
|
7
|
+
| { min: number; max: number };
|
|
8
|
+
|
|
9
|
+
type TabindexNavigator = {
|
|
10
|
+
next: (delta: number) => void;
|
|
11
|
+
set: (value: number) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function resolveBounds(bounds: TabindexBounds): { min: number; max: number; size: number } | null {
|
|
15
|
+
if ("count" in bounds) {
|
|
16
|
+
const count = bounds.count();
|
|
17
|
+
if (!Number.isFinite(count) || count <= 0) return null;
|
|
18
|
+
const min = bounds.min ?? 0;
|
|
19
|
+
const max = min + count - 1;
|
|
20
|
+
return { min, max, size: count };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const min = bounds.min;
|
|
24
|
+
const max = bounds.max;
|
|
25
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || max < min) return null;
|
|
26
|
+
return { min, max, size: max - min + 1 };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeValue(
|
|
30
|
+
value: number,
|
|
31
|
+
current: number,
|
|
32
|
+
bounds: { min: number; max: number; size: number },
|
|
33
|
+
mode: TabindexBoundaryMode
|
|
34
|
+
): number {
|
|
35
|
+
if (mode === "clamp") {
|
|
36
|
+
return Math.min(bounds.max, Math.max(bounds.min, value));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (mode === "none") {
|
|
40
|
+
return value < bounds.min || value > bounds.max ? current : value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// wrap
|
|
44
|
+
const size = bounds.size;
|
|
45
|
+
if (size <= 0) return current;
|
|
46
|
+
const offset = value - bounds.min;
|
|
47
|
+
const wrapped = ((offset % size) + size) % size;
|
|
48
|
+
return bounds.min + wrapped;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createTabindexNavigator(
|
|
52
|
+
tabindex: WritableSignal<number>,
|
|
53
|
+
bounds: TabindexBounds,
|
|
54
|
+
mode: TabindexBoundaryMode = "wrap"
|
|
55
|
+
): TabindexNavigator {
|
|
56
|
+
const applyValue = (value: number) => {
|
|
57
|
+
const current = tabindex();
|
|
58
|
+
const resolved = resolveBounds(bounds);
|
|
59
|
+
if (!resolved) return;
|
|
60
|
+
const nextValue = normalizeValue(value, current, resolved, mode);
|
|
61
|
+
if (nextValue !== current) {
|
|
62
|
+
tabindex.set(nextValue);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
next: (delta: number) => applyValue(tabindex() + delta),
|
|
68
|
+
set: (value: number) => applyValue(value),
|
|
69
|
+
};
|
|
70
|
+
}
|
package/testing/index.ts
CHANGED
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
import { bootstrapCanvas, Canvas, ComponentInstance, Element, h } from "canvasengine";
|
|
2
|
+
import type { Application } from "pixi.js";
|
|
2
3
|
|
|
3
4
|
export class TestBed {
|
|
4
|
-
static
|
|
5
|
+
private static lastApp: Application | null = null;
|
|
6
|
+
|
|
7
|
+
static async createComponent(
|
|
8
|
+
component: any,
|
|
9
|
+
props: any = {},
|
|
10
|
+
children: any = [],
|
|
11
|
+
options: { enableLayout?: boolean } = {}
|
|
12
|
+
): Promise<Element<ComponentInstance>> {
|
|
13
|
+
if (TestBed.lastApp) {
|
|
14
|
+
try {
|
|
15
|
+
TestBed.lastApp.destroy(
|
|
16
|
+
{ removeView: true },
|
|
17
|
+
{ children: true, texture: true, textureSource: true, context: true }
|
|
18
|
+
);
|
|
19
|
+
} catch {
|
|
20
|
+
// ignore cleanup errors in test environment
|
|
21
|
+
}
|
|
22
|
+
TestBed.lastApp = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const root = document.getElementById('root');
|
|
26
|
+
if (root) {
|
|
27
|
+
root.innerHTML = '';
|
|
28
|
+
}
|
|
29
|
+
|
|
5
30
|
const comp = () => h(Canvas, {
|
|
6
31
|
tickStart: false
|
|
7
32
|
}, h(component, props, children))
|
|
8
|
-
const { canvasElement, app } = await bootstrapCanvas(
|
|
33
|
+
const { canvasElement, app } = await bootstrapCanvas(root, comp, {
|
|
34
|
+
enableLayout: options.enableLayout ?? true
|
|
35
|
+
})
|
|
9
36
|
app.render()
|
|
37
|
+
TestBed.lastApp = app as Application;
|
|
10
38
|
return canvasElement.props.children?.[0]
|
|
11
39
|
}
|
|
12
|
-
}
|
|
40
|
+
}
|
package/tsconfig.json
CHANGED