Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/components/common/abort-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* A utility class that wraps AbortController, allowing its signal to be
* used for event listeners and providing a mechanism to reset it,
* effectively generating a fresh AbortController instance on subsequent access
* after an abort call.
*/
class AbortHandle {
private _controller: AbortController;

constructor() {
this._controller = new AbortController();
}

/**
* Returns the AbortSignal associated with the current AbortController instance.
* This signal can be passed to functions like `addEventListener` or `fetch`.
*/
public get signal(): AbortSignal {
return this._controller.signal;
}

/**
* Aborts the current AbortController instance and immediately creates a new,
* fresh AbortController.
*
* Any operations or event listeners associated with the previous signal
* will be aborted. Subsequent accesses to `signal` will return the
* signal from the new controller.
*/
public abort(reason?: unknown): void {
this._controller.abort(reason);
this._controller = new AbortController();
}

/**
* Resets the controller without triggering an abort.
* This is useful if you want to explicitly get a fresh signal without
* aborting any ongoing operations from the previous signal.
*/
public reset(): void {
this._controller = new AbortController();
}
}

/**
* Creates and returns an `AbortHandle` object that wraps an AbortController,
* providing a resettable AbortSignal. This allows you to use the signal for event
* listeners, fetch requests, or other cancellable operations, and then
* reset the underlying AbortController to get a fresh signal without
* needing to create a new wrapper object.
*/
export function createAbortHandle(): AbortHandle {
return new AbortHandle();
}
19 changes: 11 additions & 8 deletions src/components/common/controllers/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
import type { Ref } from 'lit/directives/ref.js';

import { getDefaultLayer } from '../../resize-container/default-ghost.js';
import { createAbortHandle } from '../abort-handler.js';
import {
findElementFromEventPath,
getRoot,
Expand Down Expand Up @@ -105,14 +106,16 @@ const additionalEvents = [
] as const;

class DragController implements ReactiveController {
private _host: ReactiveControllerHost & LitElement;
private _options: DragControllerConfiguration = {
private readonly _host: ReactiveControllerHost & LitElement;
private readonly _options: DragControllerConfiguration = {
enabled: true,
mode: 'deferred',
snapToCursor: false,
layer: getDefaultLayer,
};

private readonly _abortHandle = createAbortHandle();

private _state!: State;

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

/** @internal */
public hostConnected(): void {
this._host.addEventListener('dragstart', this);
this._host.addEventListener('touchstart', this, { passive: false });
this._host.addEventListener('pointerdown', this);
const { signal } = this._abortHandle;

this._host.addEventListener('dragstart', this, { signal });
this._host.addEventListener('touchstart', this, { passive: false, signal });
this._host.addEventListener('pointerdown', this, { signal });
}

/** @internal */
public hostDisconnected(): void {
this._host.removeEventListener('dragstart', this);
this._host.removeEventListener('touchstart', this);
this._host.removeEventListener('pointerdown', this);
this._abortHandle.abort();
this._setDragCancelListener(false);
this._removeGhost();
}
Expand Down
10 changes: 6 additions & 4 deletions src/components/common/controllers/focus-ring.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ReactiveController, ReactiveControllerHost } from 'lit';
import { createAbortHandle } from '../abort-handler.js';

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

private readonly _host: ReactiveControllerHost & HTMLElement;
private readonly _abortHandle = createAbortHandle();
private _isKeyboardFocused = false;

/**
Expand All @@ -41,16 +43,16 @@ class KeyboardFocusRingController implements ReactiveController {

/** @internal */
public hostConnected(): void {
const { signal } = this._abortHandle;

for (const event of KeyboardFocusRingController._events) {
this._host.addEventListener(event, this, { passive: true });
this._host.addEventListener(event, this, { passive: true, signal });
}
}

/** @internal */
public hostDisconnected(): void {
for (const event of KeyboardFocusRingController._events) {
this._host.removeEventListener(event, this);
}
this._abortHandle.abort();
}

/** @internal */
Expand Down
43 changes: 26 additions & 17 deletions src/components/common/controllers/gestures.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReactiveController, ReactiveControllerHost } from 'lit';
import type { Ref } from 'lit/directives/ref.js';
import { createAbortHandle } from '../abort-handler.js';

const Events = [
'pointerdown',
Expand Down Expand Up @@ -72,7 +73,9 @@ export class SwipeEvent extends Event {

class GesturesController extends EventTarget implements ReactiveController {
private readonly _host: ReactiveControllerHost & HTMLElement;
private _ref?: Ref<HTMLElement>;
private readonly _ref?: Ref<HTMLElement>;
private readonly _abortHandle = createAbortHandle();

private _options: GesturesOptions = {
thresholdDistance: 100,
thresholdTime: 500,
Expand All @@ -90,7 +93,7 @@ class GesturesController extends EventTarget implements ReactiveController {
}

/** Get the current configuration object */
public get options() {
public get options(): GesturesOptions {
return this._options;
}

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

this.addEventListener(type, bound, options);
return this;
}

public handleEvent(event: PointerEvent) {
/** @internal */
public handleEvent(event: PointerEvent): void {
if (this._options.touchOnly && event.pointerType === 'mouse') {
return;
}

switch (event.type) {
case 'pointerdown':
return this._handlePointerDown(event);
this._handlePointerDown(event);
break;
case 'pointermove':
return this._handlePointerMove(event);
this._handlePointerMove(event);
break;
case 'lostpointercapture':
case 'pointercancel':
return this._handleLostPointerCapture(event);
this._handleLostPointerCapture(event);
}
}

public async hostConnected() {
await this._host.updateComplete;
/** @internal */
public hostConnected(): void {
const { signal } = this._abortHandle;

for (const event of Events) {
this._element.addEventListener(event, this, { passive: true });
}
this._host.updateComplete.then(() => {
for (const event of Events) {
this._element.addEventListener(event, this, { passive: true, signal });
}
});
}

public hostDisconnected() {
for (const event of Events) {
this._element.removeEventListener(event, this);
}
/** @internal */
public hostDisconnected(): void {
this._abortHandle.abort();
}

/** Updates the configuration of the controller */
public updateOptions(options: Omit<GesturesOptions, 'ref'>) {
public updateOptions(options: Omit<GesturesOptions, 'ref'>): void {
Object.assign(this._options, options);
}

Expand Down
10 changes: 6 additions & 4 deletions src/components/common/controllers/key-bindings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReactiveController, ReactiveControllerHost } from 'lit';
import type { Ref } from 'lit/directives/ref.js';
import { createAbortHandle } from '../abort-handler.js';
import { asArray, findElementFromEventPath, isFunction } from '../util.js';

//#region Keys and modifiers
Expand Down Expand Up @@ -182,6 +183,7 @@ class KeyBindingController implements ReactiveController {

private readonly _host: ReactiveControllerHost & Element;
private readonly _ref?: Ref;
private readonly _abortHandle = createAbortHandle();

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

/** @internal */
public hostConnected(): void {
this._host.addEventListener('keyup', this);
this._host.addEventListener('keydown', this);
const { signal } = this._abortHandle;
this._host.addEventListener('keyup', this, { signal });
this._host.addEventListener('keydown', this, { signal });
}

/** @internal */
public hostDisconnected(): void {
this._host.removeEventListener('keyup', this);
this._host.removeEventListener('keydown', this);
this._abortHandle.abort();
}

/** @internal */
Expand Down
16 changes: 9 additions & 7 deletions src/components/resize-container/resize-controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactiveController, ReactiveControllerHost } from 'lit';

import { createAbortHandle } from '../common/abort-handler.js';
import { findElementFromEventPath } from '../common/util.js';
import { createDefaultGhostElement, getDefaultLayer } from './default-ghost.js';
import type { ResizeControllerConfiguration, ResizeState } from './types.js';
Expand All @@ -16,8 +16,10 @@ type State = {
};

class ResizeController implements ReactiveController {
private _host: ReactiveControllerHost & HTMLElement;
private _options: ResizeControllerConfiguration = {
private readonly _host: ReactiveControllerHost & HTMLElement;
private readonly _abortHandle = createAbortHandle();

private readonly _options: ResizeControllerConfiguration = {
enabled: true,
layer: getDefaultLayer,
};
Expand Down Expand Up @@ -94,14 +96,14 @@ class ResizeController implements ReactiveController {

/** @internal */
public hostConnected(): void {
this._host.addEventListener('pointerdown', this);
this._host.addEventListener('touchstart', this, { passive: false });
const { signal } = this._abortHandle;
this._host.addEventListener('pointerdown', this, { signal });
this._host.addEventListener('touchstart', this, { passive: false, signal });
}

/** @internal */
public hostDisconnected(): void {
this._host.removeEventListener('pointerdown', this);
this._host.removeEventListener('touchstart', this);
this._abortHandle.abort();
this._setResizeCancelListener(false);
this._removeGhostElement();
}
Expand Down
Loading