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-element-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@playcanvas/react": patch
---

`<Application>` now creates an `ElementInput`, so elements with `useInput`, buttons, scroll views and scrollbars respond to the mouse, touch and XR input sources. It is created before the mouse and touch devices, so calling `stopPropagation()` in a UI event handler also keeps the event from `app.mouse` and `app.touch`.
26 changes: 25 additions & 1 deletion packages/lib/src/Application.test.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import type { Application as PlayCanvasApplication } from 'playcanvas';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import '@testing-library/jest-dom';
// eslint-disable-next-line import-x/order -- preserve the existing side-effect import order
import React from 'react';

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

describe('Application', () => {
beforeEach(() => {
vi.clearAllMocks();
});

// Without an ElementInput, elements with useInput, buttons and scroll views get no input
it('creates an ElementInput for the UI', async () => {
const onApp = vi.fn();
const CaptureApp = () => {
const app = useApp();
React.useEffect(() => onApp(app), [app]);
return null;
};

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

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

const app = onApp.mock.calls[0][0] as PlayCanvasApplication;
expect(app.elementInput).toBeTruthy();
expect(app.elementInput!.app).toBe(app);
});

it('The Application component renders with default props', () => {
render(<Application deviceTypes={['null']} />);

Expand Down
8 changes: 7 additions & 1 deletion packages/lib/src/Application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
FILLMODE_KEEP_ASPECT,
RESOLUTION_AUTO,
Application as PlayCanvasApplication,
ElementInput,
Mouse,
TouchDevice,
RESOLUTION_FIXED,
Expand Down Expand Up @@ -159,8 +160,13 @@ export const ApplicationWithoutCanvas: FC<ApplicationWithoutCanvasProps> = (prop
return;
}

// Proceed with normal PlayCanvas init
// Proceed with normal PlayCanvas init. The ElementInput gives <Element useInput> and
// the button, scroll view and scrollbar components their input. It is created before
// the mouse and touch devices so that its listeners run first, which lets a UI event
// handler that calls stopPropagation() keep the event from them too.
const elementInput = new ElementInput(canvas);
const pcApp = new PlayCanvasApplication(canvas, {
elementInput,
mouse: new Mouse(canvas),
touch: new TouchDevice(canvas),
graphicsDevice: dev
Expand Down
9 changes: 5 additions & 4 deletions packages/lib/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@ afterEach(() => {
vi.clearAllMocks();
});

// Create test application with spies
const createTestApp = (actual: typeof playcanvas) => {
// Create test application with spies. The element input the component creates is passed through,
// so that tests can check what it gives the UI.
const createTestApp = (actual: typeof playcanvas, options?: { elementInput?: playcanvas.ElementInput }) => {
const canvas = document.createElement('canvas');

const app = new actual.Application(canvas, {
graphicsDevice: new actual.NullGraphicsDevice(canvas),
elementInput: options?.elementInput,
touch: new actual.TouchDevice(canvas),
mouse: new actual.Mouse(canvas)
});
Expand Down Expand Up @@ -75,8 +77,7 @@ vi.mock('playcanvas', async () => {
const actual = await vi.importActual<typeof playcanvas>('playcanvas');

const MockApplication = vi.fn(function MockApplication(this: unknown, ...args: unknown[]) {
void args;
return createTestApp(actual);
return createTestApp(actual, args[1] as { elementInput?: playcanvas.ElementInput } | undefined);
});

return {
Expand Down
Loading