Skip to content

Commit

Permalink
[web] Add keyboard support to gesture handler (#3035)
Browse files Browse the repository at this point in the history
## Description

By default, all native android and web components support keyboard
navigation and option to press elements with `Enter` or `Spacebar` keys.

Gestures and components provided by Gesture Handler lack the latter
option.

This PR adds keyboard support to the entirety of gesture handler,
allowing it to replace mouse or finger navigation.

## Test plan

- try navigating around the example app using just the following keys:
`Tab`, `Shift-Tab`, `Space`, `Enter`

## Gallery

<details>
<summary>
Expand
</summary>


https://github.com/user-attachments/assets/3523142a-c786-4dc2-aa84-f49d96d03ecd



https://github.com/user-attachments/assets/488c7716-91ed-42aa-8a6c-49a90db5002a
</details>
  • Loading branch information
latekvo authored Aug 9, 2024
1 parent acf996d commit 6d645fc
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 2 deletions.
6 changes: 4 additions & 2 deletions docs/docs/gestures/_shared/base-gesture-event-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ Current [state](/docs/fundamentals/states-events) of the handler. Expressed as o
Represents the number of pointers (fingers) currently placed on the screen.

### `pointerType`

Indicates the type of pointer device in use. This value is represented by the `PointerType` enum, which includes the following fields:

- `TOUCH` - represents finger
- `TOUCH` - represents finger
- `STYLUS` - represents stylus or digital pen
- `MOUSE` - represents computer mouse
- `OTHER` - represents unknown device type that is not relevant
- `KEY` - represents keyboard
- `OTHER` - represents unknown device type that is not relevant
1 change: 1 addition & 0 deletions src/PointerType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export enum PointerType {
TOUCH,
STYLUS,
MOUSE,
KEY,
OTHER,
}
2 changes: 2 additions & 0 deletions src/web/tools/GestureHandlerWebDelegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { isPointerInBounds } from '../utils';
import EventManager from './EventManager';
import { Config } from '../interfaces';
import { MouseButton } from '../../handlers/gestureHandlerCommon';
import KeyboardEventManager from './KeyboardEventManager';

export class GestureHandlerWebDelegate
implements GestureHandlerDelegate<HTMLElement, IGestureHandler>
Expand Down Expand Up @@ -46,6 +47,7 @@ export class GestureHandlerWebDelegate

this.eventManagers.push(new PointerEventManager(this.view));
this.eventManagers.push(new TouchEventManager(this.view));
this.eventManagers.push(new KeyboardEventManager(this.view));

this.eventManagers.forEach((manager) =>
this.gestureHandler.attachEventManager(manager)
Expand Down
91 changes: 91 additions & 0 deletions src/web/tools/KeyboardEventManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { AdaptedEvent, EventTypes } from '../interfaces';
import EventManager from './EventManager';
import { PointerType } from '../../PointerType';

export default class KeyboardEventManager extends EventManager<HTMLElement> {
private activationKeys = ['Enter', ' '];
private cancelationKeys = ['Tab'];
private isPressed = false;

private keyDownCallback = (event: KeyboardEvent): void => {
if (this.cancelationKeys.indexOf(event.key) !== -1 && this.isPressed) {
this.dispatchEvent(event, EventTypes.CANCEL);
return;
}

if (this.activationKeys.indexOf(event.key) === -1) {
return;
}

this.dispatchEvent(event, EventTypes.DOWN);
};

private keyUpCallback = (event: KeyboardEvent): void => {
if (this.activationKeys.indexOf(event.key) === -1 || !this.isPressed) {
return;
}

this.dispatchEvent(event, EventTypes.UP);
};

private dispatchEvent(event: KeyboardEvent, eventType: EventTypes) {
if (!(event.target instanceof HTMLElement)) {
return;
}

const adaptedEvent = this.mapEvent(event, eventType);

switch (eventType) {
case EventTypes.UP:
this.isPressed = false;
this.onPointerUp(adaptedEvent);
break;
case EventTypes.DOWN:
this.isPressed = true;
this.onPointerDown(adaptedEvent);
break;
case EventTypes.CANCEL:
this.isPressed = false;
this.onPointerCancel(adaptedEvent);
break;
}
}

public registerListeners(): void {
this.view.addEventListener('keydown', this.keyDownCallback);
this.view.addEventListener('keyup', this.keyUpCallback);
}

public unregisterListeners(): void {
this.view.addEventListener('keydown', this.keyDownCallback);
this.view.addEventListener('keyup', this.keyUpCallback);
}

protected mapEvent(
event: KeyboardEvent,
eventType: EventTypes
): AdaptedEvent {
const viewRect = (event.target as HTMLElement).getBoundingClientRect();

const viewportPosition = {
x: viewRect?.x + viewRect?.width / 2,
y: viewRect?.y + viewRect?.height / 2,
};

const relativePosition = {
x: viewRect?.width / 2,
y: viewRect?.height / 2,
};

return {
x: viewportPosition.x,
y: viewportPosition.y,
offsetX: relativePosition.x,
offsetY: relativePosition.y,
pointerId: 0,
eventType: eventType,
pointerType: PointerType.KEY,
time: event.timeStamp,
};
}
}

0 comments on commit 6d645fc

Please sign in to comment.