Skip to content

Commit

Permalink
feat: rework long press slightly
Browse files Browse the repository at this point in the history
Reworks a few things:

- Refactor the code structure to match that of other directives
- Rewrite the tests (was faster to do this than figure out the gaps)
- Add documentation
  • Loading branch information
43081j committed Jun 12, 2024
1 parent d72d946 commit cb752b3
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 103 deletions.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The following is the full list of available utilities:
- [keyBinding](./keyBinding.md) - key bindings (shortcuts) manager
- [localStorage](./localStorage.md) - items in [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)
- [markdown](./markdown.md) - markdown processing via [marked](https://github.com/markedjs/marked)
- [onLongPress](./onLongPress.md) - fire callback on long press
- [permissions](./permissions.md) - track the state of a browser [permission](https://developer.mozilla.org/en-US/docs/Web/API/Permissions/query)
- [propertyHistory](./propertyHistory.md) - track the history of a property with undo/redo
- [sessionStorage](./sessionStorage.md) - items in [sessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)
Expand Down
30 changes: 30 additions & 0 deletions docs/onLongPress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# onLongPress

`onLongPress` allows you to bind a callback to a long press occurring (i.e.
a pointer being held down for a specified amount of time).

## Usage

```ts
class MyElement extends LitElement {
render() {
return html`
<div ${onLongPress(this._onLongPress)}>
Long press me!
</div>
`;
}
}
```

This will call the `_onLongPress` method when the user holds their pointer
down for 1 second (the default).

The parameters (`onLongPress(fn, time)`) are as follows:

- `fn` - a function to call when the pointer has been held long enough
- `time` - the time in milliseconds to consider a press being 'long'

## Options

N/A
168 changes: 110 additions & 58 deletions src/directives/onLongPress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,47 @@ import type {
PartInfo
} from 'lit/async-directive.js';

export type LongPressCallback = (event?: PointerEvent) => void;
export type LongPressCallback = (event: PointerEvent) => void;

const DEFAULT_LONG_PRESS_TIMEOUT_MS = 1000 as number;
const DEFAULT_LONG_PRESS_TIMEOUT_MS: number = 1000;

/**
* Calls a callback when the pointer has been held down for a specified
* duration
*/
class LongPressDirective extends AsyncDirective {
/** Element of the directive. */
#element?: Element;
private __element?: Element;

/**
* Long press timeout.
* This timeout initiates when the pointer is pressed on the
* element. It calls user's callback unless cancel events occur
* before time's out.
*/
#longPressTimeout?: number;
private __longPressTimer?: number;

/** Time before the timeout runs out. */
#longPressTimeoutMs?: number | undefined;
private __longPressTimeoutMs: number = DEFAULT_LONG_PRESS_TIMEOUT_MS;

/** User-defined callback for long-press event */
#longPressCallback?: LongPressCallback;
private __longPressCallback?: LongPressCallback;

/** @inheritdoc */
public constructor(partInfo: PartInfo) {
super(partInfo);

if (partInfo.type !== PartType.ELEMENT) {
throw new Error(
'The `onLongPress` directive must be used in an element binding'
);
}
this.#updateElement((partInfo as ElementPart).element);
}

/** @inheritdoc */
public render(
_callback: LongPressCallback,
_callbackTimeoutMs: number = DEFAULT_LONG_PRESS_TIMEOUT_MS
_callbackTimeoutMs?: number
): unknown {
return noChange;
}
Expand All @@ -53,50 +57,95 @@ class LongPressDirective extends AsyncDirective {
part: ElementPart,
[callback, callbackTimeoutMs]: DirectiveParameters<this>
): unknown {
if (part.element !== this.#element) {
this.#updateElement(part.element);
if (part.element !== this.__element) {
this.__setElement(part.element);
}
this.#longPressCallback = callback;
this.#longPressTimeoutMs = callbackTimeoutMs;

this.__longPressCallback = callback;
this.__longPressTimeoutMs =
callbackTimeoutMs ?? DEFAULT_LONG_PRESS_TIMEOUT_MS;

return this.render(callback, callbackTimeoutMs);
}

#updateElement(element: Element) {
/**
* Sets the element and its handlers
* @param {Element} element Element to set
* @return {void}
*/
private __setElement(element: Element) {
// Detach events from previous element
if (this.#element) {
this.#detachEvents(this.#element);
if (this.__element) {
this.__removeListenersFromElement(this.__element);
}
this.#element = element;
this.#attachEvents(element);
this.__element = element;
this.__addListenersToElement(element);
}

#attachEvents(node: Element) {
node.addEventListener('pointerdown', this.#onPointerDown);
node.addEventListener('pointerup', this.#onPointerUp);
node.addEventListener('pointerleave', this.#onPointerLeave);
/**
* Removes any associated listeners from the given element
* @param {Element} node Element to remove listeners from
* @return {void}
*/
private __removeListenersFromElement(node: Element): void {
// cast to get strongly typed events (sadtimes)
const element = node as HTMLElement;
element.removeEventListener('pointerdown', this.__onPointerDown);
element.removeEventListener('pointerup', this.__onPointerUp);
element.removeEventListener('pointerleave', this.__onPointerLeave);
}

#detachEvents(node: Element) {
node.removeEventListener('pointerdown', this.#onPointerDown);
node.removeEventListener('pointerup', this.#onPointerUp);
node.removeEventListener('pointerleave', this.#onPointerLeave);
/**
* Adds any associated listeners to the given element
* @param {Element} node Element to add listeners to
* @return {void}
*/
private __addListenersToElement(node: Element): void {
// cast to get strongly typed events (sadtimes)
const element = node as HTMLElement;
element.addEventListener('pointerdown', this.__onPointerDown);
element.addEventListener('pointerup', this.__onPointerUp);
element.addEventListener('pointerleave', this.__onPointerLeave);
}

// TODO: When the mouse is released and long press event
// was accepted, we should find a way to cancel the @click
// event listener if it exists.
#onPointerDown = (e: PointerEvent): void => this.#initiateTimeout(e);
#onPointerUp = (): void => this.#abort();
#onPointerLeave = (): void => this.#abort();
/**
* Fired when the pointer is down/pressed
* @param {PointerEvent} e Event fired
* @return {void}
*/
private __onPointerDown = (e: PointerEvent): void => {
// TODO: When the mouse is released and long press event
// was accepted, we should find a way to cancel the @click
// event listener if it exists.
this.__initiateTimer(e);
};

/**
* Fired when the pointer is up/released
* @return {void}
*/
private __onPointerUp = (): void => {
this.__clearTimer();
};

/**
* Fired when the pointer leaves the host
* @return {void}
*/
private __onPointerLeave = (): void => {
this.__clearTimer();
};

/**
* Start the long press timeout.
* @returns {void}
*/
#initiateTimeout(e: PointerEvent): void {
this.#longPressTimeout = setTimeout(() => {
this.#longPressCallback?.(e);
}, this.#longPressTimeoutMs ?? DEFAULT_LONG_PRESS_TIMEOUT_MS);
private __initiateTimer(e: PointerEvent): void {
this.__longPressTimer = setTimeout(() => {
if (this.__longPressCallback) {
this.__longPressCallback(e);
}
}, this.__longPressTimeoutMs);
}

/**
Expand All @@ -105,44 +154,47 @@ class LongPressDirective extends AsyncDirective {
* or when the mouse leaves the element.
* @return {void}
*/
#cancelTimeout(): void {
clearTimeout(this.#longPressTimeout);
}

/**
* Abort the long press timeout on special occasions.
* @returns {void}
*/
#abort(): void {
this.#cancelTimeout();
private __clearTimer(): void {
clearTimeout(this.__longPressTimer);
}

/** @inheritdoc */
protected override disconnected(): void {
if (this.#element) {
this.#detachEvents(this.#element);
public override reconnected(): void {
if (this.__element) {
this.__addListenersToElement(this.__element);
}
}

/** @inheritdoc */
protected override reconnected(): void {
if (this.#element) {
this.#attachEvents(this.#element);
public override disconnected(): void {
if (this.__element) {
this.__removeListenersFromElement(this.__element);
}
}
}

const onLongPressDirective = directive(LongPressDirective);

/**
* Calls the `callback` function when the user has held their pointer down
* for the specified duration (default 1s).
*
* For example:
*
* ```ts
* html`
* <div ${onLongPress(fn)}>Long press me!</div>
* `;
* ```
*
* @param {LongPressCallback} callback Function to call on long press
* @param {number=} callbackTimeoutMs Time to wait before considering the event
* to be a long press
* @return {DirectiveResult}
*/
export function onLongPress(
callback: LongPressCallback,
callbackTimeoutMs: number = DEFAULT_LONG_PRESS_TIMEOUT_MS
callbackTimeoutMs?: number
): DirectiveResult<DirectiveClass> {
return onLongPressDirective(callback, callbackTimeoutMs);
}

declare global {
interface ElementEventMap {
pointerdown: PointerEvent;
}
}
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ export * from './controllers/sessionStorage.js';
export * from './controllers/slot.js';
export * from './controllers/windowScroll.js';
export * from './directives/bindInput.js';
export * from './directives/onLongPress.js';
export * from './directives/onLongPress.js';
Loading

0 comments on commit cb752b3

Please sign in to comment.