@caperjs/core 0.2.3 → 0.2.4
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/lib/caper.mjs +15 -10
- package/lib/caper.mjs.map +1 -1
- package/lib/ui/Button.d.ts +8 -1
- package/lib/ui/Button.d.ts.map +1 -1
- package/lib/ui/Button.test.d.ts +2 -0
- package/lib/ui/Button.test.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/ui/Button.test.ts +236 -0
- package/src/ui/Button.ts +41 -10
- package/src/version.ts +1 -1
package/lib/ui/Button.d.ts
CHANGED
|
@@ -104,6 +104,7 @@ export declare class Button extends _Button implements IButton {
|
|
|
104
104
|
private _isDownListenerAdded;
|
|
105
105
|
private _pointerId?;
|
|
106
106
|
private _textLabel?;
|
|
107
|
+
private _lastClickEvent?;
|
|
107
108
|
/**
|
|
108
109
|
* The text label of the button.
|
|
109
110
|
* @returns {Text | HTMLText | BitmapText | undefined} The text label instance.
|
|
@@ -120,6 +121,12 @@ export declare class Button extends _Button implements IButton {
|
|
|
120
121
|
* @param {boolean} enabled - Whether the button is enabled.
|
|
121
122
|
*/
|
|
122
123
|
set enabled(enabled: boolean);
|
|
124
|
+
/**
|
|
125
|
+
* Clears any in-progress press: removes the window-level release listener and
|
|
126
|
+
* resets press tracking. Called when the button is disabled, removed, or
|
|
127
|
+
* destroyed mid-press so a stale pointerId can't block future presses.
|
|
128
|
+
*/
|
|
129
|
+
private _resetPressState;
|
|
123
130
|
addLabel<T extends Text | HTMLText | BitmapText>(config: (Partial<TextProps | HTMLTextProps> & {
|
|
124
131
|
type?: 'text' | 'html' | 'bitmap';
|
|
125
132
|
}) | T): T;
|
|
@@ -149,7 +156,7 @@ export declare class Button extends _Button implements IButton {
|
|
|
149
156
|
* Removes the keyup event listener and emits the onPress and onUp events.
|
|
150
157
|
*/
|
|
151
158
|
protected handlePointerUp(e: FederatedEvent): void;
|
|
152
|
-
protected handleClick(): void;
|
|
159
|
+
protected handleClick(e?: FederatedEvent): void;
|
|
153
160
|
/**
|
|
154
161
|
* Handles the pointer up event.
|
|
155
162
|
*/
|
package/lib/ui/Button.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../src/ui/Button.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,MAAM,EACN,cAAc,EACd,cAAc,EACd,qBAAqB,EACrB,QAAQ,EACR,MAAM,EACN,IAAI,EACL,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAIxE,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACpC,OAAO,EAAE,UAAU,EAAkB,KAAK,gBAAgB,EAAE,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAEhG,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;AAClE,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAA;CAAE,CAAC;AAE/D,MAAM,MAAM,sBAAsB,GAAG,YAAY,GAAG,cAAc,CAAC;AAEnE,KAAK,eAAe,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEnE,MAAM,WAAW,OAAO;IAEtB,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;IAC9B,eAAe,EAAE,MAAM,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC,CAAC;IAE/C,IAAI,EAAE,MAAM,CAAC;IAEb,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAEhB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE;QACR,OAAO,EAAE,YAAY,CAAC;QACtB,KAAK,CAAC,EAAE,YAAY,CAAC;QACrB,MAAM,CAAC,EAAE,YAAY,CAAC;QACtB,QAAQ,CAAC,EAAE,YAAY,CAAC;KACzB,CAAC;IACF,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,UAAU,CAAC;QACnB,GAAG,CAAC,EAAE,UAAU,CAAC;QACjB,IAAI,CAAC,EAAE,UAAU,CAAC;QAClB,KAAK,CAAC,EAAE,UAAU,CAAC;KACpB,CAAC;IACF,OAAO,CAAC,EAAE;QACR,KAAK,CAAC,EAAE,sBAAsB,CAAC;QAC/B,GAAG,CAAC,EAAE,sBAAsB,CAAC;QAC7B,IAAI,CAAC,EAAE,sBAAsB,CAAC;QAC9B,KAAK,CAAC,EAAE,sBAAsB,CAAC;KAChC,CAAC;IACF,SAAS,CAAC,EAAE,OAAO,CAAC,SAAS,GAAG,aAAa,CAAC,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;KAAE,CAAC;IACvF,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,gBAAgB,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC;CACzD,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,CAAC,MAAM,YAAY,CAAC,EAUlD,CAAC;AAGF,QAAA,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;uKAAiD,CAAC;AAE/D;;;;GAIG;AACH,qBAAa,MAAO,SAAQ,OAAQ,YAAW,OAAO;IAE7C,MAAM,eAAoB,IAAI,EAAI;IAClC,IAAI,eAAoB,IAAI,EAAI;IAChC,WAAW,eAAoB,IAAI,EAAI;IACvC,KAAK,eAAoB,IAAI,EAAI;IACjC,MAAM,eAAoB,IAAI,EAAI;IAClC,OAAO,eAAoB,IAAI,EAAI;IACnC,SAAS,eAAoB,IAAI,EAAI;IACrC,UAAU,eAAoB,IAAI,EAAI;IACtC,eAAe,eAAoB,MAAM,KAAK,IAAI,EAAI;IACtD,SAAS,eAAoB,IAAI,EAAI;IACrC,EAAE,EAAE,MAAM,CAAC;IAEX,IAAI,EAAE,MAAM,CAAC;IAEb,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAEvB,SAAS,CAAC,MAAM,EAAE,YAAY,CAAC;IAE/B,SAAS,CAAC,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,CAAa;IAChE,OAAO,CAAC,oBAAoB,CAAkB;IAC9C,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,UAAU,CAAC,CAA+B;
|
|
1
|
+
{"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../src/ui/Button.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Button.test.d.ts","sourceRoot":"","sources":["../../src/ui/Button.test.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Button transitively imports Application → Pixi display graph. Stub it — these
|
|
4
|
+
// tests drive the press-state machine directly and never touch the app singleton.
|
|
5
|
+
vi.mock('../core/Application', () => ({
|
|
6
|
+
Application: { getInstance: () => ({}) },
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import { Button } from './Button';
|
|
10
|
+
import { Signal } from '../signals';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Builds a Button instance without running the real constructor (which drags in
|
|
14
|
+
* Factory/Interactive/Focusable mixin construction and a real Pixi Sprite). Only
|
|
15
|
+
* the fields the handlers under test actually touch are hand-initialized.
|
|
16
|
+
*/
|
|
17
|
+
function makeButton() {
|
|
18
|
+
const button = Object.create(Button.prototype) as Button;
|
|
19
|
+
|
|
20
|
+
// Pixi's Container (via eventemitter3) normally initializes these in its own
|
|
21
|
+
// constructor, which this hand-rolled instance never runs; `on`/`off` throw
|
|
22
|
+
// without them.
|
|
23
|
+
(button as any)._events = Object.create(null);
|
|
24
|
+
(button as any)._eventsCount = 0;
|
|
25
|
+
|
|
26
|
+
(button as any).config = {
|
|
27
|
+
id: 'button',
|
|
28
|
+
textures: { default: '' },
|
|
29
|
+
sheet: undefined,
|
|
30
|
+
enabled: true,
|
|
31
|
+
cursor: 'default',
|
|
32
|
+
disabledCursor: 'not-allowed',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
(button as any)._enabled = true;
|
|
36
|
+
button.isDown = false;
|
|
37
|
+
button.isOver = false;
|
|
38
|
+
(button as any)._pointerId = undefined;
|
|
39
|
+
(button as any).isKeyDown = false;
|
|
40
|
+
(button as any).cursor = 'default';
|
|
41
|
+
(button as any).focusEnabled = true;
|
|
42
|
+
|
|
43
|
+
(button as any).view = { texture: undefined };
|
|
44
|
+
(button as any).make = { texture: vi.fn(() => 'texture') };
|
|
45
|
+
(button as any)._isDownCallbacks = new Map();
|
|
46
|
+
(button as any)._isDownListenerAdded = false;
|
|
47
|
+
|
|
48
|
+
button.onDown = new Signal();
|
|
49
|
+
button.onUp = new Signal();
|
|
50
|
+
button.onUpOutside = new Signal();
|
|
51
|
+
button.onOut = new Signal();
|
|
52
|
+
button.onOver = new Signal();
|
|
53
|
+
button.onClick = new Signal();
|
|
54
|
+
button.onEnabled = new Signal();
|
|
55
|
+
button.onDisabled = new Signal();
|
|
56
|
+
button.onKeyboardEvent = new Signal();
|
|
57
|
+
button.onDestroy = new Signal();
|
|
58
|
+
|
|
59
|
+
return button;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe('Button press-state machine', () => {
|
|
63
|
+
it('recovers from a press that is disabled mid-press', () => {
|
|
64
|
+
const button = makeButton();
|
|
65
|
+
const onDown = vi.fn();
|
|
66
|
+
button.onDown.connect(onDown);
|
|
67
|
+
|
|
68
|
+
(button as any).handlePointerDown({ pointerId: 1 });
|
|
69
|
+
expect(button.isDown).toBe(true);
|
|
70
|
+
|
|
71
|
+
button.enabled = false;
|
|
72
|
+
expect(button.isDown).toBe(false);
|
|
73
|
+
expect((button as any)._pointerId).toBeUndefined();
|
|
74
|
+
|
|
75
|
+
button.enabled = true;
|
|
76
|
+
(button as any).handlePointerDown({ pointerId: 2 });
|
|
77
|
+
|
|
78
|
+
expect(onDown).toHaveBeenCalledTimes(2);
|
|
79
|
+
expect(button.isDown).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('clears press state on pointerupoutside while disabled and does not emit onUpOutside', () => {
|
|
83
|
+
const button = makeButton();
|
|
84
|
+
const onUpOutside = vi.fn();
|
|
85
|
+
button.onUpOutside.connect(onUpOutside);
|
|
86
|
+
|
|
87
|
+
// Arm a stale press, then disable without going through the enabled setter,
|
|
88
|
+
// to exercise the handler's own cleanup path directly.
|
|
89
|
+
button.isDown = true;
|
|
90
|
+
(button as any)._pointerId = 1;
|
|
91
|
+
(button as any)._enabled = false;
|
|
92
|
+
|
|
93
|
+
(button as any).handlePointerUpOutside({ pointerId: 1 });
|
|
94
|
+
|
|
95
|
+
expect(button.isDown).toBe(false);
|
|
96
|
+
expect((button as any)._pointerId).toBeUndefined();
|
|
97
|
+
expect(onUpOutside).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('removes the window pointerup listener on destroy', () => {
|
|
101
|
+
const button = makeButton();
|
|
102
|
+
(button as any).handlePointerDown({ pointerId: 1 });
|
|
103
|
+
|
|
104
|
+
// Stub out the real mixin chain's destroy() (Focusable → Interactive →
|
|
105
|
+
// WithSignals → Factory → real Pixi Container.destroy) so this test isolates
|
|
106
|
+
// Button.destroy()'s own added cleanup without needing a fully-constructed
|
|
107
|
+
// Container underneath.
|
|
108
|
+
const superDestroy = vi
|
|
109
|
+
.spyOn(Object.getPrototypeOf(Button.prototype), 'destroy')
|
|
110
|
+
.mockImplementation(() => {});
|
|
111
|
+
const removeSpy = vi.spyOn(window, 'removeEventListener');
|
|
112
|
+
|
|
113
|
+
button.destroy();
|
|
114
|
+
|
|
115
|
+
expect(removeSpy).toHaveBeenCalledWith('pointerup', (button as any).handlePointerUpOutside);
|
|
116
|
+
expect(superDestroy).toHaveBeenCalled();
|
|
117
|
+
expect(button.isDown).toBe(false);
|
|
118
|
+
expect((button as any)._pointerId).toBeUndefined();
|
|
119
|
+
|
|
120
|
+
removeSpy.mockRestore();
|
|
121
|
+
superDestroy.mockRestore();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('does not click after a cancelled press (disabled then re-enabled)', () => {
|
|
125
|
+
const button = makeButton();
|
|
126
|
+
const onClick = vi.fn();
|
|
127
|
+
button.onClick.connect(onClick);
|
|
128
|
+
|
|
129
|
+
(button as any).handlePointerDown({ pointerId: 1 });
|
|
130
|
+
button.enabled = false;
|
|
131
|
+
button.enabled = true;
|
|
132
|
+
|
|
133
|
+
// A real pointer-driven click event carries the same pointerId as the press.
|
|
134
|
+
(button as any).handleClick({ pointerId: 1 });
|
|
135
|
+
|
|
136
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('clicks normally after a press', () => {
|
|
140
|
+
const button = makeButton();
|
|
141
|
+
const onClick = vi.fn();
|
|
142
|
+
button.onClick.connect(onClick);
|
|
143
|
+
|
|
144
|
+
(button as any).handlePointerDown({ pointerId: 1 });
|
|
145
|
+
(button as any).handleClick({ pointerId: 1 });
|
|
146
|
+
|
|
147
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('still clicks for a synthetic keyboard activation (no pointerId)', () => {
|
|
151
|
+
const button = makeButton();
|
|
152
|
+
const onClick = vi.fn();
|
|
153
|
+
button.onClick.connect(onClick);
|
|
154
|
+
|
|
155
|
+
// Mimics FocusManagerPlugin's synthetic pointerdown on Enter/Space keydown.
|
|
156
|
+
(button as any).handlePointerDown({ type: 'pointerdown' });
|
|
157
|
+
(button as any).handleClick();
|
|
158
|
+
|
|
159
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('fires onClick exactly once for a single accessibility activation delivered as click+tap', () => {
|
|
163
|
+
const button = makeButton();
|
|
164
|
+
const onClick = vi.fn();
|
|
165
|
+
button.onClick.connect(onClick);
|
|
166
|
+
|
|
167
|
+
// AccessibilitySystem dispatches ONE shared FederatedEvent instance as both
|
|
168
|
+
// 'click' and 'tap' — Button connects both to handleClick.
|
|
169
|
+
const a11yEvent = { type: 'click' };
|
|
170
|
+
(button as any).handleClick(a11yEvent);
|
|
171
|
+
(button as any).handleClick(a11yEvent);
|
|
172
|
+
|
|
173
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('suppresses a cancelled pointer-driven click even without a prior press in this test', () => {
|
|
177
|
+
const button = makeButton();
|
|
178
|
+
const onClick = vi.fn();
|
|
179
|
+
button.onClick.connect(onClick);
|
|
180
|
+
|
|
181
|
+
// isDown is false (no press) — a click event carrying a pointerId must be treated
|
|
182
|
+
// as a cancelled press and suppressed.
|
|
183
|
+
(button as any).handleClick({ pointerId: 5 });
|
|
184
|
+
|
|
185
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('fires onClick for each of two distinct accessibility activations', () => {
|
|
189
|
+
const button = makeButton();
|
|
190
|
+
const onClick = vi.fn();
|
|
191
|
+
button.onClick.connect(onClick);
|
|
192
|
+
|
|
193
|
+
(button as any).handleClick({ type: 'click' });
|
|
194
|
+
(button as any).handleClick({ type: 'click' });
|
|
195
|
+
|
|
196
|
+
expect(onClick).toHaveBeenCalledTimes(2);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('toggles the ticker listener on for the first isDown callback and off when the last is removed', () => {
|
|
200
|
+
const button = makeButton();
|
|
201
|
+
const add = vi.fn();
|
|
202
|
+
const remove = vi.fn();
|
|
203
|
+
Object.defineProperty(button, 'app', { value: { ticker: { add, remove } }, configurable: true });
|
|
204
|
+
|
|
205
|
+
button.addIsDownCallback('a', () => {});
|
|
206
|
+
button.addIsDownCallback('b', () => {});
|
|
207
|
+
|
|
208
|
+
expect(add).toHaveBeenCalledTimes(1);
|
|
209
|
+
expect(remove).not.toHaveBeenCalled();
|
|
210
|
+
|
|
211
|
+
button.removeIsDownCallback('a');
|
|
212
|
+
expect(remove).not.toHaveBeenCalled();
|
|
213
|
+
|
|
214
|
+
button.removeIsDownCallback('b');
|
|
215
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('removes the ticker listener on destroy when one is active', () => {
|
|
219
|
+
const button = makeButton();
|
|
220
|
+
const add = vi.fn();
|
|
221
|
+
const remove = vi.fn();
|
|
222
|
+
Object.defineProperty(button, 'app', { value: { ticker: { add, remove } }, configurable: true });
|
|
223
|
+
|
|
224
|
+
button.addIsDownCallback('a', () => {});
|
|
225
|
+
|
|
226
|
+
const superDestroy = vi
|
|
227
|
+
.spyOn(Object.getPrototypeOf(Button.prototype), 'destroy')
|
|
228
|
+
.mockImplementation(() => {});
|
|
229
|
+
|
|
230
|
+
button.destroy();
|
|
231
|
+
|
|
232
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
233
|
+
|
|
234
|
+
superDestroy.mockRestore();
|
|
235
|
+
});
|
|
236
|
+
});
|
package/src/ui/Button.ts
CHANGED
|
@@ -121,6 +121,7 @@ export class Button extends _Button implements IButton {
|
|
|
121
121
|
private _isDownListenerAdded: boolean = false;
|
|
122
122
|
private _pointerId?: number;
|
|
123
123
|
private _textLabel?: Text | HTMLText | BitmapText;
|
|
124
|
+
private _lastClickEvent?: FederatedEvent;
|
|
124
125
|
|
|
125
126
|
/**
|
|
126
127
|
* The text label of the button.
|
|
@@ -190,6 +191,8 @@ export class Button extends _Button implements IButton {
|
|
|
190
191
|
this.onInteraction('tap').connect(this.handleClick, -1),
|
|
191
192
|
this.onInteraction('pointerdown').connect(this.handlePointerDown, -1),
|
|
192
193
|
);
|
|
194
|
+
|
|
195
|
+
this.on('removed', this._resetPressState);
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
// enabled state
|
|
@@ -213,6 +216,7 @@ export class Button extends _Button implements IButton {
|
|
|
213
216
|
});
|
|
214
217
|
this.onEnabled.emit();
|
|
215
218
|
} else {
|
|
219
|
+
this._resetPressState();
|
|
216
220
|
this.view.texture = this.make.texture({
|
|
217
221
|
asset: this.config.textures.disabled || this.config.textures.default,
|
|
218
222
|
sheet: this.config.sheet ?? undefined,
|
|
@@ -221,6 +225,18 @@ export class Button extends _Button implements IButton {
|
|
|
221
225
|
}
|
|
222
226
|
}
|
|
223
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Clears any in-progress press: removes the window-level release listener and
|
|
230
|
+
* resets press tracking. Called when the button is disabled, removed, or
|
|
231
|
+
* destroyed mid-press so a stale pointerId can't block future presses.
|
|
232
|
+
*/
|
|
233
|
+
private _resetPressState() {
|
|
234
|
+
window.removeEventListener('pointerup', this.handlePointerUpOutside);
|
|
235
|
+
this.off('pointerupoutside', this.handlePointerUpOutside);
|
|
236
|
+
this.isDown = false;
|
|
237
|
+
this._pointerId = undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
224
240
|
public addLabel<T extends Text | HTMLText | BitmapText>(
|
|
225
241
|
config: (Partial<TextProps | HTMLTextProps> & { type?: 'text' | 'html' | 'bitmap' }) | T,
|
|
226
242
|
): T {
|
|
@@ -264,6 +280,11 @@ export class Button extends _Button implements IButton {
|
|
|
264
280
|
|
|
265
281
|
destroy(options?: DestroyOptions) {
|
|
266
282
|
this.onDestroy.emit();
|
|
283
|
+
this._resetPressState();
|
|
284
|
+
if (this._isDownListenerAdded) {
|
|
285
|
+
this.app.ticker.remove(this._handleIsDownCallbacks);
|
|
286
|
+
this._isDownListenerAdded = false;
|
|
287
|
+
}
|
|
267
288
|
super.destroy(options);
|
|
268
289
|
}
|
|
269
290
|
|
|
@@ -290,6 +311,7 @@ export class Button extends _Button implements IButton {
|
|
|
290
311
|
|
|
291
312
|
removeIsDownCallback(callbackId: string) {
|
|
292
313
|
this._isDownCallbacks.delete(callbackId);
|
|
314
|
+
this._checkIsDownCallbacks();
|
|
293
315
|
}
|
|
294
316
|
|
|
295
317
|
setTexture(textureId: ButtonTextureId, texture: TextureAsset) {
|
|
@@ -395,10 +417,20 @@ export class Button extends _Button implements IButton {
|
|
|
395
417
|
this._pointerId = undefined;
|
|
396
418
|
}
|
|
397
419
|
|
|
398
|
-
protected handleClick() {
|
|
420
|
+
protected handleClick(e?: FederatedEvent) {
|
|
399
421
|
if (!this._enabled) {
|
|
400
422
|
return;
|
|
401
423
|
}
|
|
424
|
+
const pointerId = (e as FederatedPointerEvent | undefined)?.pointerId;
|
|
425
|
+
if (pointerId !== undefined && !this.isDown) {
|
|
426
|
+
// a pointer press that was cancelled (e.g. disabled mid-press) must not click
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (e !== undefined && e === this._lastClickEvent) {
|
|
430
|
+
// accessibility dispatches one event instance as both 'click' and 'tap'
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
this._lastClickEvent = e;
|
|
402
434
|
this.isDown = false;
|
|
403
435
|
this.onClick.emit();
|
|
404
436
|
if (this.config.sounds?.click) {
|
|
@@ -413,17 +445,16 @@ export class Button extends _Button implements IButton {
|
|
|
413
445
|
* Handles the pointer up event.
|
|
414
446
|
*/
|
|
415
447
|
protected handlePointerUpOutside(e: PointerEvent | FederatedPointerEvent) {
|
|
416
|
-
if (
|
|
448
|
+
if (e.pointerId !== this._pointerId) {
|
|
417
449
|
return;
|
|
418
450
|
}
|
|
419
|
-
|
|
420
|
-
this.
|
|
421
|
-
this.view.texture = this.make.texture({ asset: this.config.textures.default, sheet: this.config.sheet });
|
|
422
|
-
this.isDown = false;
|
|
451
|
+
const wasEnabled = this._enabled;
|
|
452
|
+
this._resetPressState();
|
|
423
453
|
this.isOver = false;
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
454
|
+
if (wasEnabled) {
|
|
455
|
+
this.view.texture = this.make.texture({ asset: this.config.textures.default, sheet: this.config.sheet });
|
|
456
|
+
this.onUpOutside.emit();
|
|
457
|
+
}
|
|
427
458
|
}
|
|
428
459
|
|
|
429
460
|
private _doAction(action: ButtonActionOrCallback) {
|
|
@@ -442,7 +473,7 @@ export class Button extends _Button implements IButton {
|
|
|
442
473
|
if (!this._isDownListenerAdded && this._isDownCallbacks.size > 0) {
|
|
443
474
|
this._isDownListenerAdded = true;
|
|
444
475
|
this.app.ticker.add(this._handleIsDownCallbacks);
|
|
445
|
-
} else {
|
|
476
|
+
} else if (this._isDownListenerAdded && this._isDownCallbacks.size === 0) {
|
|
446
477
|
this.app.ticker.remove(this._handleIsDownCallbacks);
|
|
447
478
|
this._isDownListenerAdded = false;
|
|
448
479
|
}
|
package/src/version.ts
CHANGED