diff --git a/examples/public/focus-tray.riv b/examples/public/focus-tray.riv new file mode 100644 index 00000000..92875e36 Binary files /dev/null and b/examples/public/focus-tray.riv differ diff --git a/examples/public/key-and-text-listeners.riv b/examples/public/key-and-text-listeners.riv new file mode 100644 index 00000000..48f1fb64 Binary files /dev/null and b/examples/public/key-and-text-listeners.riv differ diff --git a/examples/public/multiline-text-input.riv b/examples/public/multiline-text-input.riv new file mode 100644 index 00000000..ce24db98 Binary files /dev/null and b/examples/public/multiline-text-input.riv differ diff --git a/examples/public/text_input_semantics.riv b/examples/public/text_input_semantics.riv new file mode 100644 index 00000000..a303f62a Binary files /dev/null and b/examples/public/text_input_semantics.riv differ diff --git a/examples/src/components/DirectionalFocus.stories.ts b/examples/src/components/DirectionalFocus.stories.ts new file mode 100644 index 00000000..0f7cab9f --- /dev/null +++ b/examples/src/components/DirectionalFocus.stories.ts @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import DirectionalFocus from './DirectionalFocus'; + +const meta = { + title: 'Directional Focus', + component: DirectionalFocus, + parameters: { + layout: 'fullscreen', + }, + args: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Grid: Story = { + name: 'Grid 1–9 (arrow keys)', +}; diff --git a/examples/src/components/DirectionalFocus.tsx b/examples/src/components/DirectionalFocus.tsx new file mode 100644 index 00000000..015d6b1c --- /dev/null +++ b/examples/src/components/DirectionalFocus.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useRive } from '@rive-app/react-canvas'; + +const DirectionalFocus = () => { + const { RiveComponent } = useRive({ + src: 'focus-tray.riv', + artboard: 'Grid', + stateMachine: 'State Machine 1', + autoBind: true, + autoplay: true, + tabIndex: 0, + }); + + return ( +
+

+ Click the canvas or Tab into it, then use arrow keys to move focus + between the buttons; Enter activates. +

+ +
+ ); +}; + +export default DirectionalFocus; diff --git a/examples/src/components/TextInput.stories.tsx b/examples/src/components/TextInput.stories.tsx new file mode 100644 index 00000000..143203e8 --- /dev/null +++ b/examples/src/components/TextInput.stories.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { LoginForm, MultilineAndListeners } from './TextInput'; + +const meta = { + title: 'Text Input', + component: LoginForm, + parameters: { + layout: 'fullscreen', + }, + args: { semantics: true }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const LoginFormSemantics: Story = { + name: 'Login form (semantics)', +}; + +export const LoginFormNoSemantics: Story = { + name: 'Login form (no semantics)', + args: { semantics: false }, +}; + +export const MultilineAndListenersStory: Story = { + name: 'Multiline + listeners', + render: () => , +}; diff --git a/examples/src/components/TextInput.tsx b/examples/src/components/TextInput.tsx new file mode 100644 index 00000000..f6214e2e --- /dev/null +++ b/examples/src/components/TextInput.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { useRive, SemanticMode } from '@rive-app/react-canvas'; + +/** + * Text input fields and keyboard/text listeners. Rive handles keyboard input + * once its canvas is focused, so every canvas here gets `tabIndex: 0`. + */ + +// Inline (not a shared CSSProperties) because RiveComponent's style type comes +// from the root package's @types/react. +const canvasStyle = (height: number) => ({ + position: 'relative' as const, + width: 480, + height, + border: '1px solid #ccc', + margin: '12px 0', +}); + +const styles: Record = { + page: { fontFamily: 'system-ui, sans-serif', padding: 24 }, + description: { maxWidth: 640, lineHeight: 1.5 }, + row: { display: 'flex', flexWrap: 'wrap', gap: 16 }, +}; + +// `position: relative` on the container is required for the semantics overlay. +const LoginFormCanvas = ({ semantics }: { semantics: boolean }) => { + const { RiveComponent } = useRive({ + src: 'text_input_semantics.riv', + stateMachine: 'State Machine 1', + autoplay: true, + autoBind: true, + tabIndex: 0, + ...(semantics ? { semanticsMode: SemanticMode.Enabled } : {}), + }); + return ; +}; + +export const LoginForm = ({ semantics }: { semantics: boolean }) => ( +
+

+ {semantics + ? 'Semantics enabled: fields are mirrored by hidden DOM inputs, so screen readers and the browser Tab order see them. ' + : 'Semantics disabled: the canvas is focusable and Rive handles focus between its own fields. '} + Click the Email field and type, try Backspace, Cmd/Ctrl+A and Cmd/Ctrl+C, + then Tab to Password (obscured) and on to the After button. Shift+Tab + walks back out through Before to the page button above the canvas. +

+ + + +
+); + +const ListenerCanvas = ({ src }: { src: string }) => { + const { RiveComponent } = useRive({ + src, + artboard: 'Main', + stateMachine: 'State Machine 1', + autoplay: true, + autoBind: true, + tabIndex: 0, + }); + return ; +}; + +export const MultilineAndListeners = () => ( +
+

+ Left: a single-line Title and a multiline Message field, two-way bound to + view model readouts. Type in each; Enter inserts a newline in Message + only. Right: Both has keyboard and text listeners with + counters, + KeysOnly has keyboard listeners only, and{' '} + Field is a native text input. Click a node, type and + press keys to watch the counters. +

+
+ + +
+
+); diff --git a/package-lock.json b/package-lock.json index a29f8809..0f7eb5b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,9 @@ "version": "4.35.0", "license": "MIT", "dependencies": { - "@rive-app/canvas": "2.43.1", - "@rive-app/canvas-lite": "2.43.1", - "@rive-app/webgl2": "2.43.1" + "@rive-app/canvas": "2.44.0", + "@rive-app/canvas-lite": "2.44.0", + "@rive-app/webgl2": "2.44.0" }, "devDependencies": { "@babel/core": "^7.18.0", @@ -1357,21 +1357,21 @@ } }, "node_modules/@rive-app/canvas": { - "version": "2.43.1", - "resolved": "https://registry.npmjs.org/@rive-app/canvas/-/canvas-2.43.1.tgz", - "integrity": "sha512-GqENtpSOK5KOnF+kmzDQ70tfEQYksSOb39jcPPAV5vj5XDJ3vekPdbqTWuQdEyOFMhJUir4iBo3On9Gy2hghtQ==", + "version": "2.44.0", + "resolved": "https://registry.npmjs.org/@rive-app/canvas/-/canvas-2.44.0.tgz", + "integrity": "sha512-ak0GGwCbuWubi6rP9qB5YPy5CxYuTY3AiuTV4i1Vi7GXhuK2XB+M+4WJMuGcYGfGLzojaVMKaxtunD5OjjGzOQ==", "license": "MIT" }, "node_modules/@rive-app/canvas-lite": { - "version": "2.43.1", - "resolved": "https://registry.npmjs.org/@rive-app/canvas-lite/-/canvas-lite-2.43.1.tgz", - "integrity": "sha512-6Hb9J3Yfz3JKgLYv1pU/K14kEpj6RUAFKJV/lI1LmogFsLJibA69TQLIk6NoNUUTDmrbqGc4/8ZrEmhpwf0Z9Q==", + "version": "2.44.0", + "resolved": "https://registry.npmjs.org/@rive-app/canvas-lite/-/canvas-lite-2.44.0.tgz", + "integrity": "sha512-ywsdoYLsC+8RIZ+aMCvHQQgGRxnBHojReaOxDqI5ZL2Y/nRKdySAbLFnSrJIAbGygdkuQdJVUNF3AF01mxGhEA==", "license": "MIT" }, "node_modules/@rive-app/webgl2": { - "version": "2.43.1", - "resolved": "https://registry.npmjs.org/@rive-app/webgl2/-/webgl2-2.43.1.tgz", - "integrity": "sha512-Er2zB3bH8CIyNoHEMLU5dAtx2Ie1t4wvxOFhTIrOfr8Cya7HE5lFzAYBozSwHtBDnZ7/6NwT4oz6CuPyR06u9g==", + "version": "2.44.0", + "resolved": "https://registry.npmjs.org/@rive-app/webgl2/-/webgl2-2.44.0.tgz", + "integrity": "sha512-BIlutEsgyPRwMLEat2dDVBannAqIH75TeCCyrYA4KjUMMM+2bFJ2qfwqJACUAsUE+EYdcQaONycfTFRQ4JGLqg==", "license": "MIT" }, "node_modules/@rollup/plugin-commonjs": { diff --git a/package.json b/package.json index 9c46928e..8d74ce46 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,9 @@ }, "homepage": "https://github.com/rive-app/rive-react#readme", "dependencies": { - "@rive-app/canvas": "2.43.1", - "@rive-app/canvas-lite": "2.43.1", - "@rive-app/webgl2": "2.43.1" + "@rive-app/canvas": "2.44.0", + "@rive-app/canvas-lite": "2.44.0", + "@rive-app/webgl2": "2.44.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"