diff --git a/.changeset/application-element-input.md b/.changeset/application-element-input.md new file mode 100644 index 00000000..8c2e3f56 --- /dev/null +++ b/.changeset/application-element-input.md @@ -0,0 +1,5 @@ +--- +"@playcanvas/react": patch +--- + +`` 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`. diff --git a/packages/lib/src/Application.test.tsx b/packages/lib/src/Application.test.tsx index 2e354657..f540bc66 100644 --- a/packages/lib/src/Application.test.tsx +++ b/packages/lib/src/Application.test.tsx @@ -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( + + + + ); + + 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(); diff --git a/packages/lib/src/Application.tsx b/packages/lib/src/Application.tsx index a499c732..f9b8a151 100644 --- a/packages/lib/src/Application.tsx +++ b/packages/lib/src/Application.tsx @@ -5,6 +5,7 @@ import { FILLMODE_KEEP_ASPECT, RESOLUTION_AUTO, Application as PlayCanvasApplication, + ElementInput, Mouse, TouchDevice, RESOLUTION_FIXED, @@ -159,8 +160,13 @@ export const ApplicationWithoutCanvas: FC = (prop return; } - // Proceed with normal PlayCanvas init + // Proceed with normal PlayCanvas init. The ElementInput gives 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 diff --git a/packages/lib/test/setup.ts b/packages/lib/test/setup.ts index 2d306385..4a65feab 100644 --- a/packages/lib/test/setup.ts +++ b/packages/lib/test/setup.ts @@ -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) }); @@ -75,8 +77,7 @@ vi.mock('playcanvas', async () => { const actual = await vi.importActual('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 {