Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[web] Add keyboard support to gesture handler #3035

Merged
merged 26 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c29fdc9
add initial functional KeyEventManager class
latekvo Aug 7, 2024
76e01e4
add key activation checks and implement event mapping to standard type
latekvo Aug 7, 2024
9f6d237
use key event manager as one of the input event managers
latekvo Aug 7, 2024
e0d5799
Merge branch 'main' into @latekvo/add-keyboard-support-to-gesture-han…
latekvo Aug 7, 2024
e866846
add initial completely working KeyEventManager
latekvo Aug 7, 2024
5564563
Merge branch '@latekvo/add-keyboard-support-to-gesture-handler' of ht…
latekvo Aug 7, 2024
2a6e1f5
remove the need for a promise
latekvo Aug 7, 2024
7b258d2
add option to cancel pointers
latekvo Aug 7, 2024
63c8153
add possibility for target not to be navigatable
latekvo Aug 7, 2024
1a923af
fix invalid event type
latekvo Aug 7, 2024
c0aa2c6
simplify event dispatching
latekvo Aug 7, 2024
edcfff9
allow numpad enter to act as an activation key as well
latekvo Aug 7, 2024
1573a90
remove unnecessary mapping function and remove unsynchronous measurem…
latekvo Aug 8, 2024
3cd0886
use event.key instead of event.code
latekvo Aug 8, 2024
044cf87
add logic preventing dispatching UP and CANCEL events when no key is …
latekvo Aug 8, 2024
324090d
remove accidental function usage
latekvo Aug 8, 2024
83fbebf
Merge branch 'main' into @latekvo/add-keyboard-support-to-gesture-han…
latekvo Aug 8, 2024
b76a0ef
fix offset fields of event
latekvo Aug 8, 2024
76bf761
rename class name as per suggestion
latekvo Aug 8, 2024
af07b9e
add null check in case target is not HTMLElement
latekvo Aug 8, 2024
0a5855f
fix malformed event check
latekvo Aug 8, 2024
aff6119
update PointerTypeMapping to include new KEY type, apply suggestion
latekvo Aug 8, 2024
a5c20e9
revert utils pointer mapping change
latekvo Aug 8, 2024
2c169a2
add pointerType docs entry
latekvo Aug 8, 2024
f72af6c
apply simplification suggestions
latekvo Aug 9, 2024
03e20fc
remove unnecessary null checks
latekvo Aug 9, 2024
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
1 change: 1 addition & 0 deletions src/PointerType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export enum PointerType {
STYLUS,
MOUSE,
OTHER,
KEY,
latekvo marked this conversation as resolved.
Show resolved Hide resolved
}
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 KeyEventManager from './KeyEventManager';

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 KeyEventManager(this.view));

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

export default class KeyEventManager extends EventManager<HTMLElement> {
latekvo marked this conversation as resolved.
Show resolved Hide resolved
private activationKeys = ['Enter', 'Space', 'NumpadEnter'];
private cancelationKeys = ['Tab'];

private keyDownCallback = (event: KeyboardEvent): void => {
console.log(event.code);
latekvo marked this conversation as resolved.
Show resolved Hide resolved
if (this.cancelationKeys.indexOf(event.code) !== -1) {
latekvo marked this conversation as resolved.
Show resolved Hide resolved
this.dispatchEvent(event, EventTypes.CANCEL);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tested a scenario where one view is focused, Enter is pressed down, tab is pressed to focus another view, and then Enter is lifted?

If I understand correctly, the first view would receive DOWN and CANCEL events, but the second one would only receive an UP event. If that's the case, have you tested how different handlers behave in that scenario?

Copy link
Contributor Author

@latekvo latekvo Aug 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this cancelation logic handles the case you've mentioned.
Without it, it was possible to press Enter, switch focus with Tab and after that it was impossible to use Enter again, and the previously pressed element stayed in a pressed down state.

As for an element only receiving the UP event, I haven't observed any concerning behaviour.
Afaik both Gesture Handler and native buttons handle such cases, but i'll fix this just to be safe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As of 044cf87 added logic preventing calling UP and CANCEL when element is not pressed down.

}

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

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

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

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

private dispatchEvent(event: KeyboardEvent, eventType: EventTypes) {
(event.target as unknown as View)?.measure?.(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are in web environment, can't we use getBoundingClientRect?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but please use

it uses getBoundingClientRect under the hood but abstracts away the implementation.

Copy link
Contributor Author

@latekvo latekvo Aug 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @m-bert and @j-piasecki thank you for your review.

I'm not sure if it's possible to use measureView inside KeyEventManager, as measureView is only present on the GestureHandlerWebDelegate, which we don't have access to.

I also don't think it would be possible to add access to it either, without creating a circular dependency, as GestureHandlerWebDelegate already imports KeyEventManager, and to attain this method KeyEventManager would have to also import GestureHandlerWebDelegate.

Please let me know if it's really necessary to measureView instead of getBoundingClientRect

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd go with getBoundingClientRect - we already use it in PointerEventManager

(_x, _y, w, h, pageX, pageY) => {
const adaptedEvent = this.adaptEvent(
event,
eventType,
pageX,
pageY,
w,
h
);
latekvo marked this conversation as resolved.
Show resolved Hide resolved
switch (eventType) {
case EventTypes.UP:
this.onPointerUp(adaptedEvent);
break;
case EventTypes.DOWN:
this.onPointerDown(adaptedEvent);
break;
case EventTypes.CANCEL:
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);
}

private adaptEvent(
event: KeyboardEvent,
eventType: EventTypes,
pageX: number,
pageY: number,
elementWidth: number,
elementHeight: number
): AdaptedEvent {
return {
x: pageX + elementWidth / 2,
y: pageY + elementHeight / 2,
offsetX: pageX + elementWidth / 2,
offsetY: pageY + elementHeight / 2,
pointerId: 0,
eventType: eventType,
pointerType: PointerType.KEY,
time: event.timeStamp,
};
}

protected mapEvent(
event: KeyboardEvent,
eventType: EventTypes
): AdaptedEvent {
// unused and disfunctional but has to be present
return {
x: 0,
y: 0,
offsetX: 0,
offsetY: 0,
pointerId: 0,
eventType: eventType,
pointerType: PointerType.KEY,
time: event.timeStamp,
};
}
}
Loading