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.
Files changed (79) hide show
  1. package/dist/components/Container.d.ts +86 -0
  2. package/dist/components/Container.d.ts.map +1 -0
  3. package/dist/components/DOMContainer.d.ts +98 -0
  4. package/dist/components/DOMContainer.d.ts.map +1 -0
  5. package/dist/components/DOMElement.d.ts +16 -5
  6. package/dist/components/DOMElement.d.ts.map +1 -1
  7. package/dist/components/DOMSprite.d.ts +108 -0
  8. package/dist/components/DOMSprite.d.ts.map +1 -0
  9. package/dist/components/DisplayObject.d.ts +94 -0
  10. package/dist/components/DisplayObject.d.ts.map +1 -0
  11. package/dist/components/FocusContainer.d.ts +129 -0
  12. package/dist/components/FocusContainer.d.ts.map +1 -0
  13. package/dist/components/Mesh.d.ts +208 -0
  14. package/dist/components/Mesh.d.ts.map +1 -0
  15. package/dist/components/Sprite.d.ts +242 -0
  16. package/dist/components/Sprite.d.ts.map +1 -0
  17. package/dist/components/Viewport.d.ts +121 -0
  18. package/dist/components/Viewport.d.ts.map +1 -0
  19. package/dist/components/index.d.ts +2 -1
  20. package/dist/components/index.d.ts.map +1 -1
  21. package/dist/directives/Controls.d.ts +4 -4
  22. package/dist/directives/Controls.d.ts.map +1 -1
  23. package/dist/directives/ControlsBase.d.ts +1 -0
  24. package/dist/directives/ControlsBase.d.ts.map +1 -1
  25. package/dist/directives/FocusNavigation.d.ts +4 -22
  26. package/dist/directives/FocusNavigation.d.ts.map +1 -1
  27. package/dist/directives/KeyboardControls.d.ts +1 -0
  28. package/dist/directives/KeyboardControls.d.ts.map +1 -1
  29. package/dist/directives/Scheduler.d.ts.map +1 -1
  30. package/dist/directives/Shake.d.ts +1 -0
  31. package/dist/directives/Shake.d.ts.map +1 -1
  32. package/dist/engine/FocusManager.d.ts +10 -9
  33. package/dist/engine/FocusManager.d.ts.map +1 -1
  34. package/dist/engine/bootstrap.d.ts +1 -0
  35. package/dist/engine/bootstrap.d.ts.map +1 -1
  36. package/dist/engine/directive.d.ts +1 -1
  37. package/dist/engine/directive.d.ts.map +1 -1
  38. package/dist/engine/reactive.d.ts.map +1 -1
  39. package/dist/hooks/useFocus.d.ts.map +1 -1
  40. package/dist/index-DaGekQUW.js +2218 -0
  41. package/dist/index-DaGekQUW.js.map +1 -0
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.global.js +3 -3
  45. package/dist/index.global.js.map +1 -1
  46. package/dist/index.js +11868 -88
  47. package/dist/index.js.map +1 -1
  48. package/dist/utils/tabindex.d.ts +16 -0
  49. package/dist/utils/tabindex.d.ts.map +1 -0
  50. package/package.json +1 -1
  51. package/src/components/DOMContainer.ts +186 -1
  52. package/src/components/DOMElement.ts +164 -37
  53. package/src/components/DOMSprite.ts +759 -0
  54. package/src/components/DisplayObject.ts +33 -7
  55. package/src/components/FocusContainer.ts +22 -26
  56. package/src/components/Sprite.ts +12 -3
  57. package/src/components/Text.ts +1 -1
  58. package/src/components/Viewport.ts +5 -5
  59. package/src/components/index.ts +2 -1
  60. package/src/directives/Controls.ts +5 -5
  61. package/src/directives/ControlsBase.ts +1 -0
  62. package/src/directives/FocusNavigation.ts +8 -146
  63. package/src/directives/KeyboardControls.ts +11 -2
  64. package/src/directives/Scheduler.ts +12 -4
  65. package/src/directives/Shake.ts +9 -6
  66. package/src/engine/FocusManager.ts +44 -29
  67. package/src/engine/bootstrap.ts +5 -2
  68. package/src/engine/directive.ts +2 -2
  69. package/src/engine/reactive.ts +84 -12
  70. package/src/hooks/useFocus.ts +2 -5
  71. package/src/index.ts +2 -1
  72. package/src/types/pixi-cull.d.ts +7 -0
  73. package/src/utils/tabindex.ts +70 -0
  74. package/testing/index.ts +31 -3
  75. package/tsconfig.json +3 -2
  76. package/dist/DebugRenderer-CSxse9YI.js +0 -172
  77. package/dist/DebugRenderer-CSxse9YI.js.map +0 -1
  78. package/dist/index-DH2ZMhYm.js +0 -13276
  79. package/dist/index-DH2ZMhYm.js.map +0 -1
@@ -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: Signal<number | null>;
29
- focusedElement: Signal<Element | null>;
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
- throttle?: number;
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: SignalOrPrimitive<number>): void {
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
- this.setIndex(containerId, index);
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
- console.warn(`No focusable element at index ${index} in container "${containerId}"`);
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
-
@@ -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) {
@@ -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, directiveName: string) {
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
+ }
@@ -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
- onMount(parent, c, index + 1);
613
- propagateContext(c);
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
- onMount(parent, component);
625
- propagateContext(component);
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
- onMount(parent, comp);
634
- propagateContext(comp);
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
- onMount(parent, value);
642
- propagateContext(value);
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
- onMount(parent, element);
651
- propagateContext(element);
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
- onMount(parent, child);
663
- await propagateContext(child);
733
+ const routed = routeDomComponent(parent, child);
734
+ onMount(parent, routed);
735
+ await propagateContext(routed);
664
736
  }
665
737
  }
666
738
 
@@ -78,7 +78,7 @@ export function useFocusChange(
78
78
  }
79
79
 
80
80
  // Set up reactive effect
81
- const subscription = effect(() => {
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
- if (subscription && typeof subscription.unsubscribe === 'function') {
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,7 @@
1
+ declare module "pixi-cull" {
2
+ export class Simple {
3
+ constructor(options?: any);
4
+ lists: Array<any>;
5
+ cull(bounds: any): void;
6
+ }
7
+ }
@@ -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 async createComponent(component: any, props: any = {}, children: any = []): Promise<Element<ComponentInstance>> {
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(document.getElementById('root'), comp)
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
@@ -5,7 +5,8 @@
5
5
  "declaration": true,
6
6
  "emitDeclarationOnly": false,
7
7
  "declarationMap": true,
8
- "rootDir": "src"
8
+ "rootDir": "src",
9
+ "module": "ESNext"
9
10
  },
10
11
  "include": [
11
12
  "src/**/*"
@@ -14,4 +15,4 @@
14
15
  "dist",
15
16
  "node_modules"
16
17
  ]
17
- }
18
+ }