Skip to content

Commit a1d32c9

Browse files
authored
feat: Added AbortHandle API (#1758)
Migrated some of the controllers to use the new API.
1 parent 03b2396 commit a1d32c9

File tree

7 files changed

+123
-62
lines changed

7 files changed

+123
-62
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* A utility class that wraps AbortController, allowing its signal to be
3+
* used for event listeners and providing a mechanism to reset it,
4+
* effectively generating a fresh AbortController instance on subsequent access
5+
* after an abort call.
6+
*/
7+
class AbortHandle {
8+
private _controller: AbortController;
9+
10+
constructor() {
11+
this._controller = new AbortController();
12+
}
13+
14+
/**
15+
* Returns the AbortSignal associated with the current AbortController instance.
16+
* This signal can be passed to functions like `addEventListener` or `fetch`.
17+
*/
18+
public get signal(): AbortSignal {
19+
return this._controller.signal;
20+
}
21+
22+
/**
23+
* Aborts the current AbortController instance and immediately creates a new,
24+
* fresh AbortController.
25+
*
26+
* Any operations or event listeners associated with the previous signal
27+
* will be aborted. Subsequent accesses to `signal` will return the
28+
* signal from the new controller.
29+
*/
30+
public abort(reason?: unknown): void {
31+
this._controller.abort(reason);
32+
this._controller = new AbortController();
33+
}
34+
35+
/**
36+
* Resets the controller without triggering an abort.
37+
* This is useful if you want to explicitly get a fresh signal without
38+
* aborting any ongoing operations from the previous signal.
39+
*/
40+
public reset(): void {
41+
this._controller = new AbortController();
42+
}
43+
}
44+
45+
/**
46+
* Creates and returns an `AbortHandle` object that wraps an AbortController,
47+
* providing a resettable AbortSignal. This allows you to use the signal for event
48+
* listeners, fetch requests, or other cancellable operations, and then
49+
* reset the underlying AbortController to get a fresh signal without
50+
* needing to create a new wrapper object.
51+
*/
52+
export function createAbortHandle(): AbortHandle {
53+
return new AbortHandle();
54+
}

src/components/common/controllers/drag.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
import type { Ref } from 'lit/directives/ref.js';
77

88
import { getDefaultLayer } from '../../resize-container/default-ghost.js';
9+
import { createAbortHandle } from '../abort-handler.js';
910
import {
1011
findElementFromEventPath,
1112
getRoot,
@@ -105,14 +106,16 @@ const additionalEvents = [
105106
] as const;
106107

107108
class DragController implements ReactiveController {
108-
private _host: ReactiveControllerHost & LitElement;
109-
private _options: DragControllerConfiguration = {
109+
private readonly _host: ReactiveControllerHost & LitElement;
110+
private readonly _options: DragControllerConfiguration = {
110111
enabled: true,
111112
mode: 'deferred',
112113
snapToCursor: false,
113114
layer: getDefaultLayer,
114115
};
115116

117+
private readonly _abortHandle = createAbortHandle();
118+
116119
private _state!: State;
117120

118121
private _matchedElement!: Element | null;
@@ -206,16 +209,16 @@ class DragController implements ReactiveController {
206209

207210
/** @internal */
208211
public hostConnected(): void {
209-
this._host.addEventListener('dragstart', this);
210-
this._host.addEventListener('touchstart', this, { passive: false });
211-
this._host.addEventListener('pointerdown', this);
212+
const { signal } = this._abortHandle;
213+
214+
this._host.addEventListener('dragstart', this, { signal });
215+
this._host.addEventListener('touchstart', this, { passive: false, signal });
216+
this._host.addEventListener('pointerdown', this, { signal });
212217
}
213218

214219
/** @internal */
215220
public hostDisconnected(): void {
216-
this._host.removeEventListener('dragstart', this);
217-
this._host.removeEventListener('touchstart', this);
218-
this._host.removeEventListener('pointerdown', this);
221+
this._abortHandle.abort();
219222
this._setDragCancelListener(false);
220223
this._removeGhost();
221224
}

src/components/common/controllers/focus-ring.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ReactiveController, ReactiveControllerHost } from 'lit';
2+
import { createAbortHandle } from '../abort-handler.js';
23

34
/**
45
* A controller class which determines whether a focus ring should be shown to indicate keyboard focus.
@@ -25,6 +26,7 @@ class KeyboardFocusRingController implements ReactiveController {
2526
] as const;
2627

2728
private readonly _host: ReactiveControllerHost & HTMLElement;
29+
private readonly _abortHandle = createAbortHandle();
2830
private _isKeyboardFocused = false;
2931

3032
/**
@@ -41,16 +43,16 @@ class KeyboardFocusRingController implements ReactiveController {
4143

4244
/** @internal */
4345
public hostConnected(): void {
46+
const { signal } = this._abortHandle;
47+
4448
for (const event of KeyboardFocusRingController._events) {
45-
this._host.addEventListener(event, this, { passive: true });
49+
this._host.addEventListener(event, this, { passive: true, signal });
4650
}
4751
}
4852

4953
/** @internal */
5054
public hostDisconnected(): void {
51-
for (const event of KeyboardFocusRingController._events) {
52-
this._host.removeEventListener(event, this);
53-
}
55+
this._abortHandle.abort();
5456
}
5557

5658
/** @internal */

src/components/common/controllers/gestures.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ReactiveController, ReactiveControllerHost } from 'lit';
22
import type { Ref } from 'lit/directives/ref.js';
3+
import { createAbortHandle } from '../abort-handler.js';
34

45
const Events = [
56
'pointerdown',
@@ -72,7 +73,9 @@ export class SwipeEvent extends Event {
7273

7374
class GesturesController extends EventTarget implements ReactiveController {
7475
private readonly _host: ReactiveControllerHost & HTMLElement;
75-
private _ref?: Ref<HTMLElement>;
76+
private readonly _ref?: Ref<HTMLElement>;
77+
private readonly _abortHandle = createAbortHandle();
78+
7679
private _options: GesturesOptions = {
7780
thresholdDistance: 100,
7881
thresholdTime: 500,
@@ -90,7 +93,7 @@ class GesturesController extends EventTarget implements ReactiveController {
9093
}
9194

9295
/** Get the current configuration object */
93-
public get options() {
96+
public get options(): GesturesOptions {
9497
return this._options;
9598
}
9699

@@ -114,44 +117,50 @@ class GesturesController extends EventTarget implements ReactiveController {
114117
type: SwipeEvents,
115118
callback: (event: SwipeEvent) => void,
116119
options?: AddEventListenerOptions
117-
) {
120+
): this {
118121
const bound = callback.bind(this._host) as EventListener;
119122

120123
this.addEventListener(type, bound, options);
121124
return this;
122125
}
123126

124-
public handleEvent(event: PointerEvent) {
127+
/** @internal */
128+
public handleEvent(event: PointerEvent): void {
125129
if (this._options.touchOnly && event.pointerType === 'mouse') {
126130
return;
127131
}
132+
128133
switch (event.type) {
129134
case 'pointerdown':
130-
return this._handlePointerDown(event);
135+
this._handlePointerDown(event);
136+
break;
131137
case 'pointermove':
132-
return this._handlePointerMove(event);
138+
this._handlePointerMove(event);
139+
break;
133140
case 'lostpointercapture':
134141
case 'pointercancel':
135-
return this._handleLostPointerCapture(event);
142+
this._handleLostPointerCapture(event);
136143
}
137144
}
138145

139-
public async hostConnected() {
140-
await this._host.updateComplete;
146+
/** @internal */
147+
public hostConnected(): void {
148+
const { signal } = this._abortHandle;
141149

142-
for (const event of Events) {
143-
this._element.addEventListener(event, this, { passive: true });
144-
}
150+
this._host.updateComplete.then(() => {
151+
for (const event of Events) {
152+
this._element.addEventListener(event, this, { passive: true, signal });
153+
}
154+
});
145155
}
146156

147-
public hostDisconnected() {
148-
for (const event of Events) {
149-
this._element.removeEventListener(event, this);
150-
}
157+
/** @internal */
158+
public hostDisconnected(): void {
159+
this._abortHandle.abort();
151160
}
152161

153162
/** Updates the configuration of the controller */
154-
public updateOptions(options: Omit<GesturesOptions, 'ref'>) {
163+
public updateOptions(options: Omit<GesturesOptions, 'ref'>): void {
155164
Object.assign(this._options, options);
156165
}
157166

src/components/common/controllers/key-bindings.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ReactiveController, ReactiveControllerHost } from 'lit';
22
import type { Ref } from 'lit/directives/ref.js';
3+
import { createAbortHandle } from '../abort-handler.js';
34
import { asArray, findElementFromEventPath, isFunction } from '../util.js';
45

56
//#region Keys and modifiers
@@ -182,6 +183,7 @@ class KeyBindingController implements ReactiveController {
182183

183184
private readonly _host: ReactiveControllerHost & Element;
184185
private readonly _ref?: Ref;
186+
private readonly _abortHandle = createAbortHandle();
185187

186188
private readonly _bindings = new Map<string, KeyBinding>();
187189
private readonly _allowedKeys = new Set<string>();
@@ -280,14 +282,14 @@ class KeyBindingController implements ReactiveController {
280282

281283
/** @internal */
282284
public hostConnected(): void {
283-
this._host.addEventListener('keyup', this);
284-
this._host.addEventListener('keydown', this);
285+
const { signal } = this._abortHandle;
286+
this._host.addEventListener('keyup', this, { signal });
287+
this._host.addEventListener('keydown', this, { signal });
285288
}
286289

287290
/** @internal */
288291
public hostDisconnected(): void {
289-
this._host.removeEventListener('keyup', this);
290-
this._host.removeEventListener('keydown', this);
292+
this._abortHandle.abort();
291293
}
292294

293295
/** @internal */

src/components/resize-container/resize-controller.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ReactiveController, ReactiveControllerHost } from 'lit';
2-
2+
import { createAbortHandle } from '../common/abort-handler.js';
33
import { findElementFromEventPath } from '../common/util.js';
44
import { createDefaultGhostElement, getDefaultLayer } from './default-ghost.js';
55
import type { ResizeControllerConfiguration, ResizeState } from './types.js';
@@ -16,8 +16,10 @@ type State = {
1616
};
1717

1818
class ResizeController implements ReactiveController {
19-
private _host: ReactiveControllerHost & HTMLElement;
20-
private _options: ResizeControllerConfiguration = {
19+
private readonly _host: ReactiveControllerHost & HTMLElement;
20+
private readonly _abortHandle = createAbortHandle();
21+
22+
private readonly _options: ResizeControllerConfiguration = {
2123
enabled: true,
2224
layer: getDefaultLayer,
2325
};
@@ -94,14 +96,14 @@ class ResizeController implements ReactiveController {
9496

9597
/** @internal */
9698
public hostConnected(): void {
97-
this._host.addEventListener('pointerdown', this);
98-
this._host.addEventListener('touchstart', this, { passive: false });
99+
const { signal } = this._abortHandle;
100+
this._host.addEventListener('pointerdown', this, { signal });
101+
this._host.addEventListener('touchstart', this, { passive: false, signal });
99102
}
100103

101104
/** @internal */
102105
public hostDisconnected(): void {
103-
this._host.removeEventListener('pointerdown', this);
104-
this._host.removeEventListener('touchstart', this);
106+
this._abortHandle.abort();
105107
this._setResizeCancelListener(false);
106108
this._removeGhostElement();
107109
}

0 commit comments

Comments
 (0)