Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/application-keyboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@playcanvas/react": patch
---

`<Application>` now creates a `Keyboard` attached to the window, so `app.keyboard` is no longer `null` and scripts and hooks can read key state, for example `app.keyboard.isPressed(KEY_RIGHT)`. It is detached when the application is destroyed on unmount.
77 changes: 77 additions & 0 deletions packages/lib/src/Application.keyboard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import type { RenderOptions } from '@testing-library/react';
import type { Application as PlayCanvasApplication } from 'playcanvas';
import { KEY_RIGHT, Keyboard } from 'playcanvas';
import { useEffect } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { Application } from './Application.tsx';
import { useApp } from './hooks/index.ts';

// These tests run against the real engine. The mock in test/setup.ts builds a stand-in application
// with its own input devices, so it never sees the keyboard that <Application> creates.
vi.unmock('playcanvas');

// Renders an application and resolves with the engine application once it has been created
const renderApplication = async (options?: RenderOptions) => {
const onApp = vi.fn();
const CaptureApp = () => {
const app = useApp();
useEffect(() => onApp(app), [app]);
return null;
};

const result = render(
<Application deviceTypes={['null']}>
<CaptureApp />
</Application>,
options
);

await waitFor(() => expect(onApp).toHaveBeenCalled());

return { ...result, app: onApp.mock.calls[0][0] as PlayCanvasApplication };
};

describe('Application keyboard', () => {
afterEach(() => {
vi.restoreAllMocks();
});

// Scripts and hooks read app.keyboard, for example to push a rigid body while a key is held
it('gives the application a keyboard that listens on the window', async () => {
const { app } = await renderApplication();

expect(app.keyboard).toBeInstanceOf(Keyboard);

fireEvent.keyDown(window, { keyCode: KEY_RIGHT });
expect(app.keyboard!.isPressed(KEY_RIGHT)).toBe(true);

fireEvent.keyUp(window, { keyCode: KEY_RIGHT });
expect(app.keyboard!.isPressed(KEY_RIGHT)).toBe(false);
});

it('leaves one keyboard attached in StrictMode and detaches it on unmount', async () => {
const attach = vi.spyOn(Keyboard.prototype, 'attach');
const detach = vi.spyOn(Keyboard.prototype, 'detach');
const attached = () => attach.mock.contexts.filter((keyboard) => !detach.mock.contexts.includes(keyboard));

// StrictMode mounts, unmounts and remounts. The first mount is either abandoned before it
// creates an application (as here, where act() flushes the remount first) or its application
// is created and then destroyed (as in a browser). Either way, one keyboard stays attached.
const { app, unmount } = await renderApplication({ reactStrictMode: true });
const keyboard = app.keyboard!;

expect(attached()).toHaveLength(1);
expect(attached()[0]).toBe(keyboard);

unmount();

expect(attached()).toHaveLength(0);
expect(app.keyboard).toBeNull();

// Key presses no longer reach it
fireEvent.keyDown(window, { keyCode: KEY_RIGHT });
expect(keyboard.isPressed(KEY_RIGHT)).toBe(false);
});
});
4 changes: 4 additions & 0 deletions packages/lib/src/Application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Application as PlayCanvasApplication,
Mouse,
TouchDevice,
Keyboard,
RESOLUTION_FIXED,
DEVICETYPE_WEBGL2,
DEVICETYPE_WEBGPU,
Expand Down Expand Up @@ -163,6 +164,9 @@ export const ApplicationWithoutCanvas: FC<ApplicationWithoutCanvasProps> = (prop
const pcApp = new PlayCanvasApplication(canvas, {
mouse: new Mouse(canvas),
touch: new TouchDevice(canvas),
// On the window, as in the Editor and <pc-app>, so key presses register without the
// canvas having focus. Destroying the app detaches it.
keyboard: new Keyboard(window),
graphicsDevice: dev
});
pcApp.start();
Expand Down
Loading