diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2affb4..4114073 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,16 +23,16 @@ npm run dev We use Storybook to deploy our examples out onto a public-facing page for folks to view and see code examples for. It also serves as the place we'll include any example suites. These story files are stored in `/examples` -To run Storybook, run the following command in the terminal: +To make sure you're using the latest version of the runtime and to see changes made to the Rive React runtime reflected in your storybook, run the following command in a separate terminal window: ``` -npm run storybook +npm run dev ``` -To see changes made to the Rive React runtime reflected in your storyook, run the following command in a separate terminal window: +To run Storybook, run the following command in the terminal: ``` -npm run dev +npm run storybook ``` ### Testing diff --git a/examples/public/Orbitron.ttf b/examples/public/Orbitron.ttf new file mode 100644 index 0000000..1fc54ce Binary files /dev/null and b/examples/public/Orbitron.ttf differ diff --git a/examples/public/font_db_test.riv b/examples/public/font_db_test.riv index 60f8c19..252bf22 100644 Binary files a/examples/public/font_db_test.riv and b/examples/public/font_db_test.riv differ diff --git a/examples/src/components/DataBindingHooks.tsx b/examples/src/components/DataBindingHooks.tsx index 51da0a7..d39ba54 100644 --- a/examples/src/components/DataBindingHooks.tsx +++ b/examples/src/components/DataBindingHooks.tsx @@ -21,7 +21,6 @@ const COLOR_PROP = 'backgroundColor'; const GLOBAL_VM_CURRENCY = 'Labels'; const CURRENCY_PROP = 'currency'; - const GlobalViewModelInstance = () => { const { rive, RiveComponent } = useRive({ src: 'global_variables_test.riv', @@ -29,14 +28,14 @@ const GlobalViewModelInstance = () => { autoplay: false, autoBind: false, }); - + // Set up the main view model instance if you need a reference to it to change any properties. // These two lines are optional here: the global hooks below trigger a bind(), and bind() fills // any unset slots (including main) with their default instance — so the default main gets bound // either way. (With autoBind:false and no globals, you'd need to set the main yourself.) const mainViewModel = useViewModel(rive, { name: 'Main' }); - useViewModelInstance(mainViewModel, {rive}); - + useViewModelInstance(mainViewModel, { rive }); + // Set up global view model instances // Note that we don't need to specify every global view model here - only the ones we want to have a reference to // to change values on. The other globals will use a default instance. @@ -47,19 +46,21 @@ const GlobalViewModelInstance = () => { { rive } ); - const globalCurrencyViewModel = useViewModel(rive, { name: GLOBAL_VM_CURRENCY }); + const globalCurrencyViewModel = useViewModel(rive, { + name: GLOBAL_VM_CURRENCY, + }); const globalCurrencyInstance = useGlobalViewModelInstance( globalCurrencyViewModel, GLOBAL_VM_CURRENCY, { rive } ); - const { value: bgColor, setValue: setBgColor } = useViewModelInstanceColor( + const { setValue: setBgColor } = useViewModelInstanceColor( COLOR_PROP, globalColorsInstance ); - const { value: currency, setValue: setCurrency } = useViewModelInstanceString( + const { setValue: setCurrency } = useViewModelInstanceString( CURRENCY_PROP, globalCurrencyInstance ); diff --git a/examples/src/components/DataBindingTests.stories.tsx b/examples/src/components/DataBindingTests.stories.tsx index e9232ba..67e2513 100644 --- a/examples/src/components/DataBindingTests.stories.tsx +++ b/examples/src/components/DataBindingTests.stories.tsx @@ -1,565 +1,702 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { within, expect, waitFor, userEvent } from '@storybook/test'; -import { StringPropertyTest, NumberPropertyTest, BooleanPropertyTest, ColorPropertyTest, EnumPropertyTest, NestedViewModelTest, TriggerPropertyTest, PersonForm, PersonInstances, ImagePropertyTest, FontPropertyTest, TodoListTest, ArtboardPropertyTest } from './DataBindingTests'; +import { + StringPropertyTest, + NumberPropertyTest, + BooleanPropertyTest, + ColorPropertyTest, + EnumPropertyTest, + NestedViewModelTest, + TriggerPropertyTest, + PersonForm, + PersonInstances, + ImagePropertyTest, + FontPropertyTest, + TodoListTest, + ArtboardPropertyTest, +} from './DataBindingTests'; const meta: Meta = { - title: 'Tests/DataBinding', - parameters: { - layout: 'centered', - }, + title: 'Tests/DataBinding', + parameters: { + layout: 'centered', + }, }; export default meta; - export const StringPropertyStory: StoryObj = { - name: 'String Property', - render: () => , - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Wait for the Rive file to load - await waitFor(() => { - expect(canvas.getByTestId('name-value')).toBeTruthy(); - }, { timeout: 3000 }); - - const nameInput = canvas.getByTestId('name-input'); - await userEvent.clear(nameInput); - - // Wait for the input to be cleared - await waitFor(() => { - expect(nameInput.value).toBe(''); - }, { timeout: 1000 }); - - await userEvent.click(nameInput); - await userEvent.paste('Test User'); - - await waitFor(() => { - expect(nameInput.value).toBe('Test User'); - }, { timeout: 2000 }); - - await waitFor(() => { - expect(canvas.getByTestId('name-value').textContent).toBe('Test User'); - }, { timeout: 2000 }); - } + name: 'String Property', + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Wait for the Rive file to load + await waitFor( + () => { + expect(canvas.getByTestId('name-value')).toBeTruthy(); + }, + { timeout: 3000 } + ); + + const nameInput = canvas.getByTestId('name-input'); + await userEvent.clear(nameInput); + + // Wait for the input to be cleared + await waitFor( + () => { + expect(nameInput.value).toBe(''); + }, + { timeout: 1000 } + ); + + await userEvent.click(nameInput); + await userEvent.paste('Test User'); + + await waitFor( + () => { + expect(nameInput.value).toBe('Test User'); + }, + { timeout: 2000 } + ); + + await waitFor( + () => { + expect(canvas.getByTestId('name-value').textContent).toBe('Test User'); + }, + { timeout: 2000 } + ); + }, }; export const NumberPropertyStory: StoryObj = { - name: 'Number Property', - render: () => , - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Wait for the Rive file to load - await waitFor(() => { - expect(canvas.getByTestId('age-value')).toBeTruthy(); - }, { timeout: 2000 }); - - const ageInput = canvas.getByTestId('age-input'); - - const currentValue = ageInput.value; - expect(currentValue).toBe('23'); - - await userEvent.click(ageInput); - await userEvent.clear(ageInput); - await waitFor(() => { - expect(ageInput.value).toBe('0'); // This is a hack to wait for the input to be cleared - }, { timeout: 1000 }); - - await userEvent.paste('42'); - - - await waitFor(() => { - expect(canvas.getByTestId('age-value').textContent).toBe('42'); - }, { timeout: 2000 }); - } + name: 'Number Property', + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Wait for the Rive file to load + await waitFor( + () => { + expect(canvas.getByTestId('age-value')).toBeTruthy(); + }, + { timeout: 2000 } + ); + + const ageInput = canvas.getByTestId('age-input'); + + const currentValue = ageInput.value; + expect(currentValue).toBe('23'); + + await userEvent.click(ageInput); + await userEvent.clear(ageInput); + await waitFor( + () => { + expect(ageInput.value).toBe('0'); // This is a hack to wait for the input to be cleared + }, + { timeout: 1000 } + ); + + await userEvent.paste('42'); + + await waitFor( + () => { + expect(canvas.getByTestId('age-value').textContent).toBe('42'); + }, + { timeout: 2000 } + ); + }, }; export const BooleanPropertyStory: StoryObj = { - name: 'Boolean Property', - render: () => , - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); + name: 'Boolean Property', + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); - // Wait for the Rive file to load + // Wait for the Rive file to load - await waitFor(() => { - expect(canvas.getByTestId('terms-value')).toBeTruthy(); - }, { timeout: 2000 }); + await waitFor( + () => { + expect(canvas.getByTestId('terms-value')).toBeTruthy(); + }, + { timeout: 2000 } + ); - const termsCheckbox = canvas.getByTestId('terms-checkbox'); + const termsCheckbox = + canvas.getByTestId('terms-checkbox'); - expect(termsCheckbox.checked).toBe(false); + expect(termsCheckbox.checked).toBe(false); - expect(canvas.getByTestId('terms-value').textContent).toBe('false'); + expect(canvas.getByTestId('terms-value').textContent).toBe('false'); - await userEvent.click(termsCheckbox); + await userEvent.click(termsCheckbox); - // Verify terms update - await waitFor(() => { - expect(canvas.getByTestId('terms-value').textContent).toBe('true'); - }); - } + // Verify terms update + await waitFor(() => { + expect(canvas.getByTestId('terms-value').textContent).toBe('true'); + }); + }, }; export const ColorPropertyStory: StoryObj = { - name: 'Color Property', - render: () => , - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Wait for the Rive file to load and the component to render - await waitFor(() => { - expect(canvas.getByTestId('color-value')).toBeTruthy(); - expect(canvas.getByTestId('set-color-red')).toBeTruthy(); - expect(canvas.getByTestId('set-color-blue')).toBeTruthy(); - }, { timeout: 5000 }); - - const numberValueDiv = canvas.getByTestId('number-value'); - const hexValueDiv = canvas.getByTestId('hex-value'); - - // Verify initial state is red - await waitFor(() => { - expect(hexValueDiv.textContent).toContain('Hex value: #ce2323'); - expect(numberValueDiv.textContent).toContain('Number value: -3267805'); - }); - - - // Change color to Blue --- - const blueButton = canvas.getByTestId('set-color-blue'); - await userEvent.click(blueButton); - - // Verify Blue State - await waitFor(() => { - expect(numberValueDiv.textContent).toContain('Number value: -16776961'); - expect(hexValueDiv.textContent).toContain('Hex value: #0000ff'); - }); - - - // Change color back to Red --- - const redButton = canvas.getByTestId('set-color-red'); - await userEvent.click(redButton); - - // Verify Red State - await waitFor(() => { - expect(numberValueDiv.textContent).toContain('Number value: -65536'); - expect(hexValueDiv.textContent).toContain('Hex value: #ff0000'); - }); - } + name: 'Color Property', + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Wait for the Rive file to load and the component to render + await waitFor( + () => { + expect(canvas.getByTestId('color-value')).toBeTruthy(); + expect(canvas.getByTestId('set-color-red')).toBeTruthy(); + expect(canvas.getByTestId('set-color-blue')).toBeTruthy(); + }, + { timeout: 5000 } + ); + + const numberValueDiv = canvas.getByTestId('number-value'); + const hexValueDiv = canvas.getByTestId('hex-value'); + + // Verify initial state is red + await waitFor(() => { + expect(hexValueDiv.textContent).toContain('Hex value: #ce2323'); + expect(numberValueDiv.textContent).toContain('Number value: -3267805'); + }); + + // Change color to Blue --- + const blueButton = canvas.getByTestId('set-color-blue'); + await userEvent.click(blueButton); + + // Verify Blue State + await waitFor(() => { + expect(numberValueDiv.textContent).toContain('Number value: -16776961'); + expect(hexValueDiv.textContent).toContain('Hex value: #0000ff'); + }); + + // Change color back to Red --- + const redButton = canvas.getByTestId('set-color-red'); + await userEvent.click(redButton); + + // Verify Red State + await waitFor(() => { + expect(numberValueDiv.textContent).toContain('Number value: -65536'); + expect(hexValueDiv.textContent).toContain('Hex value: #ff0000'); + }); + }, }; export const EnumPropertyStory: StoryObj = { - name: 'Enum Property', - render: () => , - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Wait for the Rive file to load - await waitFor(() => { - expect(canvas.getByTestId('country-value')).toBeTruthy(); - }); - - // Wait for options to be loaded - await waitFor(() => { - const countrySelect = canvas.getByTestId('country-select'); - return countrySelect.options.length > 0; - }); - - const countrySelect = canvas.getByTestId('country-select'); - - // Verify that the dropdown contains usa, japan, and canada - const optionValues = Array.from(countrySelect.options).map(option => option.value); - expect(optionValues).toContain('usa'); - expect(optionValues).toContain('japan'); - expect(optionValues).toContain('canada'); - - const currentValue = countrySelect.value; - - expect(currentValue).toBe('usa'); - - let optionToSelect = 'japan'; - - await userEvent.selectOptions(countrySelect, optionToSelect); - - await waitFor(() => { - expect(canvas.getByTestId('country-value').textContent).toBe(optionToSelect); - }); - } + name: 'Enum Property', + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Wait for the Rive file to load + await waitFor(() => { + expect(canvas.getByTestId('country-value')).toBeTruthy(); + }); + + // Wait for options to be loaded + await waitFor(() => { + const countrySelect = + canvas.getByTestId('country-select'); + return countrySelect.options.length > 0; + }); + + const countrySelect = + canvas.getByTestId('country-select'); + + // Verify that the dropdown contains usa, japan, and canada + const optionValues = Array.from(countrySelect.options).map( + (option) => option.value + ); + expect(optionValues).toContain('usa'); + expect(optionValues).toContain('japan'); + expect(optionValues).toContain('canada'); + + const currentValue = countrySelect.value; + + expect(currentValue).toBe('usa'); + + let optionToSelect = 'japan'; + + await userEvent.selectOptions(countrySelect, optionToSelect); + + await waitFor(() => { + expect(canvas.getByTestId('country-value').textContent).toBe( + optionToSelect + ); + }); + }, }; export const NestedViewModelStory: StoryObj = { - name: 'Nested ViewModel Property', - render: () => , - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - - // Wait for the Rive file to load - await waitFor(() => { - expect(canvas.getByTestId('drink-type-value')).toBeTruthy(); - }); - - // Wait for options to be loaded - await waitFor(() => { - const drinkTypeSelect = canvas.getByTestId('drink-type-select'); - return drinkTypeSelect.options.length > 0; - }, { timeout: 2000 }); - - const drinkTypeSelect = canvas.getByTestId('drink-type-select'); - const optionValues = Array.from(drinkTypeSelect.options).map(option => option.value); - expect(optionValues).toContain('Coffee'); - expect(optionValues).toContain('Tea'); - - - expect(drinkTypeSelect.value).toBe('Tea'); - - let optionToSelect = 'Coffee'; - - await userEvent.selectOptions(drinkTypeSelect, optionToSelect); - - await waitFor(() => { - expect(canvas.getByTestId('drink-type-value').textContent).toBe(optionToSelect); - }); - } + name: 'Nested ViewModel Property', + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Wait for the Rive file to load + await waitFor(() => { + expect(canvas.getByTestId('drink-type-value')).toBeTruthy(); + }); + + // Wait for options to be loaded + await waitFor( + () => { + const drinkTypeSelect = + canvas.getByTestId('drink-type-select'); + return drinkTypeSelect.options.length > 0; + }, + { timeout: 2000 } + ); + + const drinkTypeSelect = + canvas.getByTestId('drink-type-select'); + const optionValues = Array.from(drinkTypeSelect.options).map( + (option) => option.value + ); + expect(optionValues).toContain('Coffee'); + expect(optionValues).toContain('Tea'); + + expect(drinkTypeSelect.value).toBe('Tea'); + + let optionToSelect = 'Coffee'; + + await userEvent.selectOptions(drinkTypeSelect, optionToSelect); + + await waitFor(() => { + expect(canvas.getByTestId('drink-type-value').textContent).toBe( + optionToSelect + ); + }); + }, }; export const TriggerPropertyStory: StoryObj = { - name: 'Trigger Property', - render: () => , - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Wait for the Rive file to load - await waitFor(() => { - expect(canvas.getByTestId('submit-button')).toBeTruthy(); - }, { timeout: 2000 }); - - expect(canvas.getByTestId('callback-triggered').textContent).toContain('none'); - - // Trigger submit action - await userEvent.click(canvas.getByTestId('submit-button')); - - await waitFor(() => { - expect(canvas.getByTestId('callback-triggered').textContent).toContain('submit-callback'); - }); - - await userEvent.click(canvas.getByTestId('reset-button')); - - // Verify onTrigger callback works for reset - await waitFor(() => { - expect(canvas.getByTestId('callback-triggered').textContent).toContain('reset-callback'); - }); - } + name: 'Trigger Property', + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Wait for the Rive file to load + await waitFor( + () => { + expect(canvas.getByTestId('submit-button')).toBeTruthy(); + }, + { timeout: 2000 } + ); + + expect(canvas.getByTestId('callback-triggered').textContent).toContain( + 'none' + ); + + // Trigger submit action + await userEvent.click(canvas.getByTestId('submit-button')); + + await waitFor(() => { + expect(canvas.getByTestId('callback-triggered').textContent).toContain( + 'submit-callback' + ); + }); + + await userEvent.click(canvas.getByTestId('reset-button')); + + // Verify onTrigger callback works for reset + await waitFor(() => { + expect(canvas.getByTestId('callback-triggered').textContent).toContain( + 'reset-callback' + ); + }); + }, }; export const PersonInstancesStory: StoryObj = { - name: 'Person Instances', - render: () => , - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Wait for the Rive file to load - await waitFor(() => { - expect(canvas.getByTestId('instance-name')).toBeTruthy(); - expect(canvas.getByTestId('select-jane')).toBeTruthy(); - expect(canvas.getByTestId('select-default')).toBeTruthy(); - }, { timeout: 2000 }); - - // Initially should show Steve - expect(canvas.getByTestId('instance-name').textContent).toContain('Steve'); - - // Switch to Jane - const janeButton = canvas.getByTestId('select-jane'); - await userEvent.click(janeButton); - - // Verify instance changed to Jane - await waitFor(() => { - expect(canvas.getByTestId('instance-name').textContent).toContain('Jane'); - }); - - // Switch to Default instance - const defaultButton = canvas.getByTestId('select-default'); - await userEvent.click(defaultButton); - - // Verify instance changed to Default - await waitFor(() => { - expect(canvas.getByTestId('instance-name').textContent).toContain('Default'); - }); - - // Switch back to Steve - const steveButton = canvas.getByTestId('select-steve'); - await userEvent.click(steveButton); - - // Verify instance changed back to Steve - await waitFor(() => { - expect(canvas.getByTestId('instance-name').textContent).toContain('Steve'); - }); - } + name: 'Person Instances', + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Wait for the Rive file to load + await waitFor( + () => { + expect(canvas.getByTestId('instance-name')).toBeTruthy(); + expect(canvas.getByTestId('select-jane')).toBeTruthy(); + expect(canvas.getByTestId('select-default')).toBeTruthy(); + }, + { timeout: 2000 } + ); + + // Initially should show Steve + expect(canvas.getByTestId('instance-name').textContent).toContain('Steve'); + + // Switch to Jane + const janeButton = canvas.getByTestId('select-jane'); + await userEvent.click(janeButton); + + // Verify instance changed to Jane + await waitFor(() => { + expect(canvas.getByTestId('instance-name').textContent).toContain('Jane'); + }); + + // Switch to Default instance + const defaultButton = canvas.getByTestId('select-default'); + await userEvent.click(defaultButton); + + // Verify instance changed to Default + await waitFor(() => { + expect(canvas.getByTestId('instance-name').textContent).toContain( + 'Default' + ); + }); + + // Switch back to Steve + const steveButton = canvas.getByTestId('select-steve'); + await userEvent.click(steveButton); + + // Verify instance changed back to Steve + await waitFor(() => { + expect(canvas.getByTestId('instance-name').textContent).toContain( + 'Steve' + ); + }); + }, }; // A configurable form story, so we can test all the properties at once export const PersonFormStory: StoryObj = { - name: 'Complete Person Form', - render: () => , - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - - // Wait for the Rive file to load - await waitFor(() => { - expect(canvas.getByTestId('name-value')).toBeTruthy(); - }, { timeout: 2000 }); - - // Update name - const nameInput = canvas.getByTestId('name-input'); - await userEvent.clear(nameInput); - await userEvent.type(nameInput, 'Test User'); - - // Update age - const ageInput = canvas.getByTestId('age-input'); - await userEvent.clear(ageInput); - await userEvent.type(ageInput, '42'); - - // Toggle terms agreement - const termsCheckbox = canvas.getByTestId('terms-checkbox'); - await userEvent.click(termsCheckbox); - - // Change color - const colorButton = canvas.getByTestId('set-color-red'); - await userEvent.click(colorButton); - - // Change country - const countrySelect = canvas.getByTestId('country-select'); - await userEvent.selectOptions(countrySelect, 'japan'); - - // Change drink type - const drinkTypeSelect = canvas.getByTestId('drink-type-select'); - await userEvent.selectOptions(drinkTypeSelect, 'Coffee'); - - // Submit the form - const submitButton = canvas.getByTestId('submit-button'); - await userEvent.click(submitButton); - } + name: 'Complete Person Form', + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Wait for the Rive file to load + await waitFor( + () => { + expect(canvas.getByTestId('name-value')).toBeTruthy(); + }, + { timeout: 2000 } + ); + + // Update name + const nameInput = canvas.getByTestId('name-input'); + await userEvent.clear(nameInput); + await userEvent.type(nameInput, 'Test User'); + + // Update age + const ageInput = canvas.getByTestId('age-input'); + await userEvent.clear(ageInput); + await userEvent.type(ageInput, '42'); + + // Toggle terms agreement + const termsCheckbox = canvas.getByTestId('terms-checkbox'); + await userEvent.click(termsCheckbox); + + // Change color + const colorButton = canvas.getByTestId('set-color-red'); + await userEvent.click(colorButton); + + // Change country + const countrySelect = + canvas.getByTestId('country-select'); + await userEvent.selectOptions(countrySelect, 'japan'); + + // Change drink type + const drinkTypeSelect = + canvas.getByTestId('drink-type-select'); + await userEvent.selectOptions(drinkTypeSelect, 'Coffee'); + + // Submit the form + const submitButton = canvas.getByTestId('submit-button'); + await userEvent.click(submitButton); + }, }; - export const ImagePropertyStory: StoryObj = { - name: 'Image Property', - render: () => , - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Wait for the Rive file to load - await waitFor(() => { - expect(canvas.getByTestId('load-random-image')).toBeTruthy(); - expect(canvas.getByTestId('clear-image')).toBeTruthy(); - }, { timeout: 3000 }); - - const loadImageButton = canvas.getByTestId('load-random-image'); - const clearImageButton = canvas.getByTestId('clear-image'); - - expect(canvas.queryByTestId('current-image-url')).toBeNull(); - - // Load a random image - await userEvent.click(loadImageButton); - - // Wait for the image to load and URL to appear - await waitFor(() => { - expect(canvas.getByTestId('current-image-url')).toBeTruthy(); - }, { timeout: 5000 }); - - // Verify the image URL is displayed - const imageUrlElement = canvas.getByTestId('current-image-url'); - expect(imageUrlElement.textContent).toContain('Current image: https://picsum.photos'); - - // Clear the image - await userEvent.click(clearImageButton); - - // Load another image to test it works multiple times - await userEvent.click(loadImageButton); - - // Wait for the new image to load - await waitFor(() => { - expect(canvas.getByTestId('current-image-url')).toBeTruthy(); - }, { timeout: 5000 }); - } + name: 'Image Property', + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Wait for the Rive file to load + await waitFor( + () => { + expect(canvas.getByTestId('load-random-image')).toBeTruthy(); + expect(canvas.getByTestId('clear-image')).toBeTruthy(); + }, + { timeout: 3000 } + ); + + const loadImageButton = canvas.getByTestId('load-random-image'); + const clearImageButton = canvas.getByTestId('clear-image'); + + expect(canvas.queryByTestId('current-image-url')).toBeNull(); + + // Load a random image + await userEvent.click(loadImageButton); + + // Wait for the image to load and URL to appear + await waitFor( + () => { + expect(canvas.getByTestId('current-image-url')).toBeTruthy(); + }, + { timeout: 5000 } + ); + + // Verify the image URL is displayed + const imageUrlElement = canvas.getByTestId('current-image-url'); + expect(imageUrlElement.textContent).toContain( + 'Current image: https://picsum.photos' + ); + + // Clear the image + await userEvent.click(clearImageButton); + + // Load another image to test it works multiple times + await userEvent.click(loadImageButton); + + // Wait for the new image to load + await waitFor( + () => { + expect(canvas.getByTestId('current-image-url')).toBeTruthy(); + }, + { timeout: 5000 } + ); + }, }; export const FontPropertyStory: StoryObj = { - name: 'Font Property', - render: () => , - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - await waitFor(() => { - expect(canvas.getByTestId('set-font-noto-thai')).toBeTruthy(); - expect(canvas.getByTestId('set-font-noto-arabic')).toBeTruthy(); - expect(canvas.getByTestId('clear-font')).toBeTruthy(); - }, { timeout: 3000 }); - + name: 'Font Property', + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await waitFor( + () => { + expect(canvas.getByTestId('set-font-noto-thai')).toBeTruthy(); + expect(canvas.getByTestId('set-font-orbitron')).toBeTruthy(); + expect(canvas.getByTestId('clear-font')).toBeTruthy(); + }, + { timeout: 3000 } + ); + + expect(canvas.queryByTestId('current-font')).toBeNull(); + + await userEvent.click(canvas.getByTestId('set-font-orbitron')); + + await waitFor( + () => { + expect(canvas.getByTestId('current-font').textContent).toBe( + 'Current font: Orbitron' + ); + }, + { timeout: 5000 } + ); + + await userEvent.click(canvas.getByTestId('clear-font')); + + await waitFor( + () => { expect(canvas.queryByTestId('current-font')).toBeNull(); + }, + { timeout: 2000 } + ); + + await userEvent.click(canvas.getByTestId('set-font-orbitron')); + + await waitFor( + () => { + expect(canvas.getByTestId('current-font').textContent).toBe( + 'Current font: Orbitron' + ); + }, + { timeout: 5000 } + ); + }, +}; - await userEvent.click(canvas.getByTestId('set-font-noto-arabic')); - - await waitFor(() => { - expect(canvas.getByTestId('current-font').textContent).toBe( - 'Current font: Noto Sans Arabic' - ); - }, { timeout: 5000 }); - - await userEvent.click(canvas.getByTestId('clear-font')); - - await waitFor(() => { - expect(canvas.queryByTestId('current-font')).toBeNull(); - }, { timeout: 2000 }); - - await userEvent.click(canvas.getByTestId('set-font-noto-arabic')); +export const TodoListStory: StoryObj = { + name: 'Todo List Property', + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Wait for the Rive file to load + await waitFor( + () => { + expect(canvas.getByTestId('list-length')).toBeTruthy(); + }, + { timeout: 3000 } + ); + + const initialLengthText = canvas.getByTestId('list-length').textContent; + const initialCount = parseInt( + initialLengthText?.match(/Items: (\d+)/)?.[1] || '0' + ); + + // Test 1: addInstance - Add item to end + const addButton = canvas.getByTestId('add-item-button'); + await userEvent.click(addButton); + + await waitFor(() => { + expect(canvas.getByTestId('list-length').textContent).toContain( + `Items: ${initialCount + 1}` + ); + }); + + // Test 2: addInstanceAt - Add item at specific index (if we have items) + if (initialCount > 0) { + const addAtButton = canvas.getByTestId('add-item-at-button'); + await userEvent.click(addAtButton); + + await waitFor(() => { + expect(canvas.getByTestId('list-length').textContent).toContain( + `Items: ${initialCount + 2}` + ); + }); + } - await waitFor(() => { - expect(canvas.getByTestId('current-font').textContent).toBe( - 'Current font: Noto Sans Arabic' - ); - }, { timeout: 5000 }); + // Test 3: getInstanceAt - Interact with specific items + const currentCount = initialCount + (initialCount > 0 ? 2 : 1); + if (currentCount > 0) { + await waitFor(() => { + expect(canvas.getByTestId('todo-item-0')).toBeTruthy(); + }); + + // Edit the first item + const todoText = canvas.getByTestId('todo-text-0'); + await userEvent.clear(todoText); + + // Wait for the input to be cleared to avoid issues with autocomplete + await waitFor( + () => { + expect((todoText as HTMLInputElement).value).toBe(''); + }, + { timeout: 2000 } + ); + + await userEvent.click(todoText); + await userEvent.paste('Test Item'); + + await waitFor( + () => { + expect(canvas.getByTestId('todo-text-value-0').textContent).toContain( + 'Test Item' + ); + }, + { timeout: 3000 } + ); } -}; + // Test 4: swap - Swap first two items + if (currentCount >= 2) { + const firstText = + canvas.getByTestId('todo-text-0').value; + const secondText = + canvas.getByTestId('todo-text-1').value; + + const swapButton = canvas.getByTestId('swap-button'); + await userEvent.click(swapButton); + + await waitFor( + () => { + expect(canvas.getByTestId('todo-text-0')).toHaveValue(secondText); + expect(canvas.getByTestId('todo-text-1')).toHaveValue(firstText); + }, + { timeout: 3000 } + ); + } -export const TodoListStory: StoryObj = { - name: 'Todo List Property', - render: () => , - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Wait for the Rive file to load - await waitFor(() => { - expect(canvas.getByTestId('list-length')).toBeTruthy(); - }, { timeout: 3000 }); - - const initialLengthText = canvas.getByTestId('list-length').textContent; - const initialCount = parseInt(initialLengthText?.match(/Items: (\d+)/)?.[1] || '0'); - - // Test 1: addInstance - Add item to end - const addButton = canvas.getByTestId('add-item-button'); - await userEvent.click(addButton); - - await waitFor(() => { - expect(canvas.getByTestId('list-length').textContent).toContain(`Items: ${initialCount + 1}`); - }); - - // Test 2: addInstanceAt - Add item at specific index (if we have items) - if (initialCount > 0) { - const addAtButton = canvas.getByTestId('add-item-at-button'); - await userEvent.click(addAtButton); - - await waitFor(() => { - expect(canvas.getByTestId('list-length').textContent).toContain(`Items: ${initialCount + 2}`); - }); - } - - // Test 3: getInstanceAt - Interact with specific items - const currentCount = initialCount + (initialCount > 0 ? 2 : 1); - if (currentCount > 0) { - await waitFor(() => { - expect(canvas.getByTestId('todo-item-0')).toBeTruthy(); - }); - - // Edit the first item - const todoText = canvas.getByTestId('todo-text-0'); - await userEvent.clear(todoText); - - // Wait for the input to be cleared to avoid issues with autocomplete - await waitFor(() => { - expect((todoText as HTMLInputElement).value).toBe(''); - }, { timeout: 2000 }); - - await userEvent.click(todoText); - await userEvent.paste('Test Item'); - - await waitFor(() => { - expect(canvas.getByTestId('todo-text-value-0').textContent).toContain('Test Item'); - }, { timeout: 3000 }); - - } - - // Test 4: swap - Swap first two items - if (currentCount >= 2) { - const firstText = canvas.getByTestId('todo-text-0').value; - const secondText = canvas.getByTestId('todo-text-1').value; - - const swapButton = canvas.getByTestId('swap-button'); - await userEvent.click(swapButton); - - await waitFor(() => { - expect(canvas.getByTestId('todo-text-0')).toHaveValue(secondText); - expect(canvas.getByTestId('todo-text-1')).toHaveValue(firstText); - }, { timeout: 3000 }); - } - - // Test 5: removeInstance - Remove by instance reference - if (currentCount > 0) { - const removeInstanceButton = canvas.getByTestId('remove-instance-button'); - await userEvent.click(removeInstanceButton); - - await waitFor(() => { - expect(canvas.getByTestId('list-length').textContent).toContain(`Items: ${currentCount - 1}`); - }, { timeout: 3000 }); - } - - // Test 6: removeInstanceAt - Remove by index - const afterRemoveInstance = currentCount > 0 ? currentCount - 1 : 0; - if (afterRemoveInstance > 0) { - const removeIndexButton = canvas.getByTestId('remove-index-button'); - await userEvent.click(removeIndexButton); - - await waitFor(() => { - expect(canvas.getByTestId('list-length').textContent).toContain(`Items: ${afterRemoveInstance - 1}`); - }, { timeout: 3000 }); - } + // Test 5: removeInstance - Remove by instance reference + if (currentCount > 0) { + const removeInstanceButton = canvas.getByTestId('remove-instance-button'); + await userEvent.click(removeInstanceButton); + + await waitFor( + () => { + expect(canvas.getByTestId('list-length').textContent).toContain( + `Items: ${currentCount - 1}` + ); + }, + { timeout: 3000 } + ); + } + // Test 6: removeInstanceAt - Remove by index + const afterRemoveInstance = currentCount > 0 ? currentCount - 1 : 0; + if (afterRemoveInstance > 0) { + const removeIndexButton = canvas.getByTestId('remove-index-button'); + await userEvent.click(removeIndexButton); + + await waitFor( + () => { + expect(canvas.getByTestId('list-length').textContent).toContain( + `Items: ${afterRemoveInstance - 1}` + ); + }, + { timeout: 3000 } + ); } + }, }; export const ArtboardPropertyStory: StoryObj = { - name: 'Artboard Property', - render: () => , - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - // Wait for the Rive file to load - await waitFor(() => { - expect(canvas.getByTestId('set-artboard1-blue')).toBeTruthy(); - expect(canvas.getByTestId('set-artboard1-red')).toBeTruthy(); - expect(canvas.getByTestId('set-artboard1-green')).toBeTruthy(); - }, { timeout: 3000 }); - - // Initially should show None - expect(canvas.getByTestId('artboard1-current').textContent).toBe('Current: None'); - expect(canvas.getByTestId('artboard2-current').textContent).toBe('Current: None'); - - // Set artboard 1 to blue - await userEvent.click(canvas.getByTestId('set-artboard1-blue')); - await waitFor(() => { - expect(canvas.getByTestId('artboard1-current').textContent).toBe('Current: ArtboardBlue'); - }); - - // Set artboard 2 to red - await userEvent.click(canvas.getByTestId('set-artboard2-red')); - await waitFor(() => { - expect(canvas.getByTestId('artboard2-current').textContent).toBe('Current: ArtboardRed'); - }); - - // Switch artboard 1 to green - await userEvent.click(canvas.getByTestId('set-artboard1-green')); - await waitFor(() => { - expect(canvas.getByTestId('artboard1-current').textContent).toBe('Current: ArtboardGreen'); - }); - - // Switch artboard 2 to blue - await userEvent.click(canvas.getByTestId('set-artboard2-blue')); - await waitFor(() => { - expect(canvas.getByTestId('artboard2-current').textContent).toBe('Current: ArtboardBlue'); - }); - } -}; \ No newline at end of file + name: 'Artboard Property', + render: () => , + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Wait for the Rive file to load + await waitFor( + () => { + expect(canvas.getByTestId('set-artboard1-blue')).toBeTruthy(); + expect(canvas.getByTestId('set-artboard1-red')).toBeTruthy(); + expect(canvas.getByTestId('set-artboard1-green')).toBeTruthy(); + }, + { timeout: 3000 } + ); + + // Initially should show None + expect(canvas.getByTestId('artboard1-current').textContent).toBe( + 'Current: None' + ); + expect(canvas.getByTestId('artboard2-current').textContent).toBe( + 'Current: None' + ); + + // Set artboard 1 to blue + await userEvent.click(canvas.getByTestId('set-artboard1-blue')); + await waitFor(() => { + expect(canvas.getByTestId('artboard1-current').textContent).toBe( + 'Current: ArtboardBlue' + ); + }); + + // Set artboard 2 to red + await userEvent.click(canvas.getByTestId('set-artboard2-red')); + await waitFor(() => { + expect(canvas.getByTestId('artboard2-current').textContent).toBe( + 'Current: ArtboardRed' + ); + }); + + // Switch artboard 1 to green + await userEvent.click(canvas.getByTestId('set-artboard1-green')); + await waitFor(() => { + expect(canvas.getByTestId('artboard1-current').textContent).toBe( + 'Current: ArtboardGreen' + ); + }); + + // Switch artboard 2 to blue + await userEvent.click(canvas.getByTestId('set-artboard2-blue')); + await waitFor(() => { + expect(canvas.getByTestId('artboard2-current').textContent).toBe( + 'Current: ArtboardBlue' + ); + }); + }, +}; diff --git a/examples/src/components/DataBindingTests.tsx b/examples/src/components/DataBindingTests.tsx index 144205c..9342f79 100644 --- a/examples/src/components/DataBindingTests.tsx +++ b/examples/src/components/DataBindingTests.tsx @@ -1,979 +1,1161 @@ -import React, { useEffect, useState } from 'react'; - -import Rive, { - useRive, - useViewModel, - useViewModelInstance, - useViewModelInstanceBoolean, - useViewModelInstanceString, - useViewModelInstanceNumber, - useViewModelInstanceEnum, - useViewModelInstanceColor, - useViewModelInstanceTrigger, - useViewModelInstanceImage, - useViewModelInstanceFont, - decodeImage, - decodeFont, - ViewModelInstance, - useViewModelInstanceList, - useViewModelInstanceArtboard +import React, { useState, useEffect, useCallback } from 'react'; + +import { + useRive, + useViewModel, + useViewModelInstance, + useViewModelInstanceBoolean, + useViewModelInstanceString, + useViewModelInstanceNumber, + useViewModelInstanceEnum, + useViewModelInstanceColor, + useViewModelInstanceTrigger, + useViewModelInstanceImage, + useViewModelInstanceFont, + decodeImage, + decodeFont, + ViewModelInstance, + useViewModelInstanceList, + useViewModelInstanceArtboard, + Layout, + Fit, } from '@rive-app/react-webgl2'; - export const StringPropertyTest = ({ src }: { src: string }) => { - const { rive, RiveComponent } = useRive({ - src, - autoplay: true, - artboard: "Artboard", - autoBind: true, - stateMachines: "State Machine 1", - }); - - const { value: name, setValue: setName } = useViewModelInstanceString('name', rive?.viewModelInstance); - - return ( + const { rive, RiveComponent } = useRive({ + src, + autoplay: true, + artboard: 'Artboard', + autoBind: true, + stateMachines: 'State Machine 1', + }); + + const { value: name, setValue: setName } = useViewModelInstanceString( + 'name', + rive?.viewModelInstance + ); + + return ( +
+ + {rive === null ? ( +
Loading…
+ ) : (
- - {(rive === null) ?
Loading…
: ( -
- -
{name}
-
- )} + +
{name}
- ); + )} +
+ ); }; export const NumberPropertyTest = ({ src }: { src: string }) => { - const { rive, RiveComponent } = useRive({ - src, - autoplay: true, - artboard: "Artboard", - autoBind: true, - stateMachines: "State Machine 1", - }); - - const { value: age, setValue: setAge } = useViewModelInstanceNumber('age', rive?.viewModelInstance); - - - return ( + const { rive, RiveComponent } = useRive({ + src, + autoplay: true, + artboard: 'Artboard', + autoBind: true, + stateMachines: 'State Machine 1', + }); + + const { value: age, setValue: setAge } = useViewModelInstanceNumber( + 'age', + rive?.viewModelInstance + ); + + return ( +
+ + {rive === null ? ( +
Loading…
+ ) : (
- - {(rive === null) ?
Loading…
: ( -
- -
{age}
-
- )} + +
{age}
- ); + )} +
+ ); }; export const BooleanPropertyTest = ({ src }: { src: string }) => { - const { rive, RiveComponent } = useRive({ - src, - autoplay: true, - artboard: "Artboard", - autoBind: true, - stateMachines: "State Machine 1", - }); - - const { value: agreedToTerms, setValue: setAgreedToTerms } = useViewModelInstanceBoolean('agreedToTerms', rive?.viewModelInstance); - - return ( + const { rive, RiveComponent } = useRive({ + src, + autoplay: true, + artboard: 'Artboard', + autoBind: true, + stateMachines: 'State Machine 1', + }); + + const { value: agreedToTerms, setValue: setAgreedToTerms } = + useViewModelInstanceBoolean('agreedToTerms', rive?.viewModelInstance); + + return ( +
+ + {rive === null ? ( +
Loading…
+ ) : (
- - {(rive === null) ?
Loading…
: ( -
- -
{agreedToTerms ? 'true' : 'false'}
-
- )} + +
+ {agreedToTerms ? 'true' : 'false'} +
- ); + )} +
+ ); }; const colorNumberToHexString = (colorNum: number | null) => { - if (colorNum === null) { - return 'N/A'; - } - const unsignedInt = colorNum >>> 0; - const r = (unsignedInt >> 16) & 0xff; - const g = (unsignedInt >> 8) & 0xff; - const b = unsignedInt & 0xff; - - const toHex = (c: number) => c.toString(16).padStart(2, '0'); - return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + if (colorNum === null) { + return 'N/A'; + } + const unsignedInt = colorNum >>> 0; + const r = (unsignedInt >> 16) & 0xff; + const g = (unsignedInt >> 8) & 0xff; + const b = unsignedInt & 0xff; + + const toHex = (c: number) => c.toString(16).padStart(2, '0'); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; }; export const ColorPropertyTest = ({ src }: { src: string }) => { - const { rive, RiveComponent } = useRive({ - src, - autoplay: true, - artboard: "Artboard", - autoBind: true, - stateMachines: "State Machine 1", - }); - - - const { value: colorNum, setValue: setColor, setRgb } = useViewModelInstanceColor('favColor', rive?.viewModelInstance); - - return ( + const { rive, RiveComponent } = useRive({ + src, + autoplay: true, + artboard: 'Artboard', + autoBind: true, + stateMachines: 'State Machine 1', + }); + + const { value: colorNum, setRgb } = useViewModelInstanceColor( + 'favColor', + rive?.viewModelInstance + ); + + return ( +
+ + {rive === null ? ( +
Loading…
+ ) : (
- - {(rive === null) ?
Loading…
: ( -
- - - -
- )} + + +
- ); + )} +
+ ); }; export const EnumPropertyTest = ({ src }: { src: string }) => { - const { rive, RiveComponent } = useRive({ - src, - autoplay: true, - artboard: "Artboard", - autoBind: true, - stateMachines: "State Machine 1", - }); - - const { value: country, setValue: setCountry, values: countries } = useViewModelInstanceEnum('country', rive?.viewModelInstance); - - return ( + const { rive, RiveComponent } = useRive({ + src, + autoplay: true, + artboard: 'Artboard', + autoBind: true, + stateMachines: 'State Machine 1', + }); + + const { + value: country, + setValue: setCountry, + values: countries, + } = useViewModelInstanceEnum('country', rive?.viewModelInstance); + + return ( +
+ + {rive === null ? ( +
Loading…
+ ) : (
- - {(rive === null) ?
Loading…
: ( -
- -
{country}
-
- )} + +
{country}
- ); + )} +
+ ); }; export const NestedViewModelTest = ({ src }: { src: string }) => { - const { rive, RiveComponent } = useRive({ - src, - autoplay: true, - artboard: "Artboard", - autoBind: true, - stateMachines: "State Machine 1", - }); - - const { value: drinkType, setValue: setDrinkType, values: drinkTypes } = useViewModelInstanceEnum('favDrink/type', rive?.viewModelInstance); - - return ( + const { rive, RiveComponent } = useRive({ + src, + autoplay: true, + artboard: 'Artboard', + autoBind: true, + stateMachines: 'State Machine 1', + }); + + const { + value: drinkType, + setValue: setDrinkType, + values: drinkTypes, + } = useViewModelInstanceEnum('favDrink/type', rive?.viewModelInstance); + + return ( +
+ + {rive === null ? ( +
Loading…
+ ) : (
- - {(rive === null) ?
Loading…
: ( -
- -
{drinkType}
-
- )} + +
{drinkType}
- ); + )} +
+ ); }; export const TriggerPropertyTest = ({ src }: { src: string }) => { - const [callbackTriggered, setCallbackTriggered] = useState(''); - - const { rive, RiveComponent } = useRive({ - src, - autoplay: true, - autoBind: true, - artboard: "Artboard", - stateMachines: "State Machine 1", - }); - - - - const { trigger: onFormSubmit } = useViewModelInstanceTrigger('onFormSubmit', rive?.viewModelInstance, - { - onTrigger: () => { - setCallbackTriggered('submit-callback'); - } - } - ); - - const { trigger: onFormReset } = useViewModelInstanceTrigger('onFormReset', rive?.viewModelInstance, - { - onTrigger: () => { - setCallbackTriggered('reset-callback'); - } - } - ); - - const handleSubmit = () => { - onFormSubmit(); - }; - - const handleReset = () => { - onFormReset(); - }; - - return ( + const [callbackTriggered, setCallbackTriggered] = useState(''); + + const { rive, RiveComponent } = useRive({ + src, + autoplay: true, + autoBind: true, + artboard: 'Artboard', + stateMachines: 'State Machine 1', + }); + + const { trigger: onFormSubmit } = useViewModelInstanceTrigger( + 'onFormSubmit', + rive?.viewModelInstance, + { + onTrigger: () => { + setCallbackTriggered('submit-callback'); + }, + } + ); + + const { trigger: onFormReset } = useViewModelInstanceTrigger( + 'onFormReset', + rive?.viewModelInstance, + { + onTrigger: () => { + setCallbackTriggered('reset-callback'); + }, + } + ); + + const handleSubmit = () => { + onFormSubmit(); + }; + + const handleReset = () => { + onFormReset(); + }; + + return ( +
+ + {rive === null ? ( +
Loading…
+ ) : (
- - {(rive === null) ?
Loading…
: ( -
- - - -
- Last callback triggered: {callbackTriggered || 'none'} -
-
- )} + + + +
+ Last callback triggered: {callbackTriggered || 'none'} +
- ); + )} +
+ ); }; export const PersonForm = ({ src }: { src: string }) => { - const { rive, RiveComponent } = useRive({ - src, - autoplay: true, - autoBind: true, - artboard: "Artboard", - stateMachines: "State Machine 1", - }); - - const { value: name, setValue: setName } = useViewModelInstanceString('name', rive?.viewModelInstance); - const { value: age, setValue: setAge } = useViewModelInstanceNumber('age', rive?.viewModelInstance); - const { value: agreedToTerms, setValue: setAgreedToTerms } = useViewModelInstanceBoolean('agreedToTerms', rive?.viewModelInstance); - const { value: colorNum, setRgb } = useViewModelInstanceColor('favColor', rive?.viewModelInstance); - const { value: country, setValue: setCountry, values: countries } = useViewModelInstanceEnum('country', rive?.viewModelInstance); - const { trigger: onFormSubmit } = useViewModelInstanceTrigger('onFormSubmit', rive?.viewModelInstance); - const { trigger: onFormReset } = useViewModelInstanceTrigger('onFormReset', rive?.viewModelInstance); - - - // Drink properties (nested viewmodel) - const { value: drinkType, setValue: setDrinkType, values: drinkTypes } = useViewModelInstanceEnum('favDrink/type', rive?.viewModelInstance); - - const handleReset = () => { - setName(''); - setAge(0); - setAgreedToTerms(false); - setRgb(0, 0, 0); - setCountry(countries[0]); - setDrinkType(drinkTypes[0]); - onFormReset(); - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - onFormSubmit(); - }; - - return ( -
- - - {(rive === null) ?
Loading…
: ( -
-
- -
{name}
-
- -
- -
{age}
-
- -
- -
{agreedToTerms ? 'true' : 'false'}
-
- -
- - - -
- -
- -
{country}
-
- -
- -
{drinkType}
-
- -
- - -
-
- )} -
- ); + const { rive, RiveComponent } = useRive({ + src, + autoplay: true, + autoBind: true, + artboard: 'Artboard', + stateMachines: 'State Machine 1', + }); + + const { value: name, setValue: setName } = useViewModelInstanceString( + 'name', + rive?.viewModelInstance + ); + const { value: age, setValue: setAge } = useViewModelInstanceNumber( + 'age', + rive?.viewModelInstance + ); + const { value: agreedToTerms, setValue: setAgreedToTerms } = + useViewModelInstanceBoolean('agreedToTerms', rive?.viewModelInstance); + const { value: colorNum, setRgb } = useViewModelInstanceColor( + 'favColor', + rive?.viewModelInstance + ); + const { + value: country, + setValue: setCountry, + values: countries, + } = useViewModelInstanceEnum('country', rive?.viewModelInstance); + const { trigger: onFormSubmit } = useViewModelInstanceTrigger( + 'onFormSubmit', + rive?.viewModelInstance + ); + const { trigger: onFormReset } = useViewModelInstanceTrigger( + 'onFormReset', + rive?.viewModelInstance + ); + + // Drink properties (nested viewmodel) + const { + value: drinkType, + setValue: setDrinkType, + values: drinkTypes, + } = useViewModelInstanceEnum('favDrink/type', rive?.viewModelInstance); + + const handleReset = () => { + setName(''); + setAge(0); + setAgreedToTerms(false); + setRgb(0, 0, 0); + setCountry(countries[0]); + setDrinkType(drinkTypes[0]); + onFormReset(); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onFormSubmit(); + }; + + return ( +
+ + + {rive === null ? ( +
Loading…
+ ) : ( +
+
+ +
{name}
+
+ +
+ +
{age}
+
+ +
+ +
+ {agreedToTerms ? 'true' : 'false'} +
+
+ +
+ + + +
+ +
+ +
{country}
+
+ +
+ +
{drinkType}
+
+ +
+ + +
+
+ )} +
+ ); }; - -// Component to demonstrate different viewmodel instances +// Component to demonstrate different view model instances export const PersonInstances = ({ src }: { src: string }) => { - const [activeInstance, setActiveInstance] = useState('Steve'); - const [useDefaultInstance, setUseDefaultInstance] = useState(false); - - const { rive, RiveComponent } = useRive({ - src, - autoplay: true, - artboard: "Artboard", - stateMachines: "State Machine 1", - }); - - const viewModel = useViewModel(rive, { name: 'PersonViewModel' }); - const params = useDefaultInstance ? { useDefault: true, rive } : { name: activeInstance, rive } - const viewModelInstance = useViewModelInstance(viewModel, params); - - - const { value: name } = useViewModelInstanceString('name', viewModelInstance); - const { value: age } = useViewModelInstanceNumber('age', viewModelInstance); - const { value: country } = useViewModelInstanceEnum('country', viewModelInstance); - - const switchToNamedInstance = (instanceName: string) => { - setActiveInstance(instanceName); - setUseDefaultInstance(false); - }; - - const switchToDefaultInstance = () => { - setUseDefaultInstance(true); - }; - - return ( + const [activeInstance, setActiveInstance] = useState('Steve'); + const [useDefaultInstance, setUseDefaultInstance] = useState(false); + + const { rive, RiveComponent } = useRive({ + src, + autoplay: true, + artboard: 'Artboard', + stateMachines: 'State Machine 1', + }); + + const viewModel = useViewModel(rive, { name: 'PersonViewModel' }); + const params = useDefaultInstance + ? { useDefault: true, rive } + : { name: activeInstance, rive }; + const viewModelInstance = useViewModelInstance(viewModel, params); + + const { value: name } = useViewModelInstanceString('name', viewModelInstance); + const { value: age } = useViewModelInstanceNumber('age', viewModelInstance); + const { value: country } = useViewModelInstanceEnum( + 'country', + viewModelInstance + ); + + const switchToNamedInstance = (instanceName: string) => { + setActiveInstance(instanceName); + setUseDefaultInstance(false); + }; + + const switchToDefaultInstance = () => { + setUseDefaultInstance(true); + }; + + return ( +
+ + + {rive === null ? ( +
Loading…
+ ) : (
- - - {(rive === null) ?
Loading…
: ( -
- - - -
- )} - -
-

Instance: {useDefaultInstance ? 'Default' : activeInstance}

-

Name: {name}

-

Age: {age}

-

Country: {country}

-
+ + +
- ); + )} + +
+

+ Instance: {useDefaultInstance ? 'Default' : activeInstance} +

+

Name: {name}

+

Age: {age}

+

Country: {country}

+
+
+ ); }; export const ImagePropertyTest = ({ src }: { src: string }) => { - const [currentImageUrl, setCurrentImageUrl] = useState(''); - const [isLoading, setIsLoading] = useState(false); - - const { rive, RiveComponent } = useRive({ - src, - artboard: "Artboard", - stateMachines: "State Machine 1", - autoplay: true, - autoBind: false, - }); - - const viewModel = useViewModel(rive, { name: 'Post' }); - const viewModelInstance = useViewModelInstance(viewModel, { rive }); - - const { setValue: setImage } = useViewModelInstanceImage( - 'image', - viewModelInstance - ); - - const loadRandomImage = async () => { - if (!setImage) return; - - setIsLoading(true); - try { - const imageUrl = `https://picsum.photos/400/300?random=${Date.now()}`; - setCurrentImageUrl(imageUrl); - - const response = await fetch(imageUrl); - const imageBuffer = await response.arrayBuffer(); - const decodedImage = await decodeImage(new Uint8Array(imageBuffer)); - - setImage(decodedImage); - - decodedImage.unref(); - } catch (error) { - console.error('Failed to load image:', error); - } finally { - setIsLoading(false); - } - }; - - const clearImage = () => { - if (setImage) { - setImage(null); - setCurrentImageUrl(''); - } - }; - - - return ( -
-
- -
+ const [currentImageUrl, setCurrentImageUrl] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const { rive, RiveComponent } = useRive({ + src, + artboard: 'Artboard', + stateMachines: 'State Machine 1', + autoplay: true, + autoBind: false, + }); + + const viewModel = useViewModel(rive, { name: 'Post' }); + const viewModelInstance = useViewModelInstance(viewModel, { rive }); + + const { setValue: setImage } = useViewModelInstanceImage( + 'image', + viewModelInstance + ); + + const loadRandomImage = async () => { + if (!setImage) return; + + setIsLoading(true); + try { + const imageUrl = `https://picsum.photos/400/300?random=${Date.now()}`; + setCurrentImageUrl(imageUrl); + + const response = await fetch(imageUrl); + const imageBuffer = await response.arrayBuffer(); + const decodedImage = await decodeImage(new Uint8Array(imageBuffer)); + + setImage(decodedImage); + + decodedImage.unref(); + } catch (error) { + console.error('Failed to load image:', error); + } finally { + setIsLoading(false); + } + }; + + const clearImage = () => { + if (setImage) { + setImage(null); + setCurrentImageUrl(''); + } + }; + + return ( +
+
+ +
+ + {rive === null ? ( +
Loading…
+ ) : ( +
+ + + +
+ )} - {rive === null ? ( -
Loading…
- ) : ( -
- - - -
- )} - - {currentImageUrl && ( -
- Current image: {currentImageUrl} -
- )} + {currentImageUrl && ( +
+ + Current image: {currentImageUrl} +
- ); + )} +
+ ); }; const FONT_OPTIONS = [ - { - name: 'Noto Serif Thai', - url: 'https://raw.githubusercontent.com/google/fonts/main/ofl/notoserifthai/NotoSerifThai%5Bwdth%2Cwght%5D.ttf', - testId: 'set-font-noto-thai', - }, - { - name: 'Noto Sans Arabic', - url: './NotoSansArabic-VariableFont_wdth,wght.ttf', - testId: 'set-font-noto-arabic', - }, + { + name: 'Orbitron', + url: './Orbitron.ttf', + testId: 'set-font-orbitron', + }, + { + name: 'Noto Serif Thai', + url: 'https://raw.githubusercontent.com/google/fonts/main/ofl/notoserifthai/NotoSerifThai%5Bwdth%2Cwght%5D.ttf', + testId: 'set-font-noto-thai', + }, ] as const; export const FontPropertyTest = ({ src }: { src: string }) => { - const [currentFont, setCurrentFont] = useState(''); - const [isLoading, setIsLoading] = useState(false); - - const { rive, RiveComponent } = useRive({ - src, - stateMachines: 'State Machine 1', - autoplay: true, - autoBind: true, - }); - - const { setValue: setFont } = useViewModelInstanceFont( - 'fontProperty', - rive?.viewModelInstance - ); - - const loadFont = async (name: string, url: string) => { - if (!setFont) return; - - setIsLoading(true); - try { - const response = await fetch(url); - const fontBuffer = await response.arrayBuffer(); - const decodedFont = await decodeFont(new Uint8Array(fontBuffer)); - - setFont(decodedFont); - setCurrentFont(name); - - decodedFont.unref(); - } catch (error) { - console.error('Failed to load font:', error); - } finally { - setIsLoading(false); - } - }; - - const clearFont = () => { - if (setFont) { - setFont(null); - setCurrentFont(''); - } - }; - - return ( -
-
- -
+ const [currentFont, setCurrentFont] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const { rive, RiveComponent } = useRive({ + src, + stateMachines: 'State Machine 1', + autoplay: true, + autoBind: true, + layout: new Layout({ + fit: Fit.Layout, + layoutScaleFactor: 0.5, + }), + }); + + const { setValue: setHeadingFont } = useViewModelInstanceFont( + 'headingFont', + rive?.viewModelInstance + ); + const { setValue: setSubheadingFont } = useViewModelInstanceFont( + 'subheadingFont', + rive?.viewModelInstance + ); + + const loadFont = useCallback( + async (name: string, url: string) => { + if (!setHeadingFont) return; + + setIsLoading(true); + try { + const response = await fetch(url); + const fontBuffer = await response.arrayBuffer(); + const decodedFont = await decodeFont(new Uint8Array(fontBuffer)); + + setHeadingFont(decodedFont); + setSubheadingFont(decodedFont); + setCurrentFont(name); + + decodedFont.unref(); + } catch (error) { + console.error('Failed to load font:', error); + } finally { + setIsLoading(false); + } + }, + [setHeadingFont, setSubheadingFont] + ); + + const clearFont = () => { + if (setHeadingFont) { + setHeadingFont(null); + setSubheadingFont(null); + setCurrentFont(''); + } + }; + + useEffect(() => { + if (rive) { + loadFont(FONT_OPTIONS[0].name, FONT_OPTIONS[0].url); + } + }, [loadFont, rive]); + + return ( +
+
+ +
+ + {rive === null ? ( +
Loading…
+ ) : ( +
+ {FONT_OPTIONS.map((font) => ( + + ))} + + +
+ )} - {rive === null ? ( -
Loading…
- ) : ( -
- {FONT_OPTIONS.map((font) => ( - - ))} - - -
- )} - - {currentFont && ( -
- Current font: {currentFont} -
- )} + {currentFont && ( +
+ Current font: {currentFont}
- ); + )} +
+ ); }; // List Property Test const TodoItemComponent = ({ - index, - todoItem + index, + todoItem, }: { - index: number; - todoItem: ViewModelInstance | null; + index: number; + todoItem: ViewModelInstance | null; }) => { - const { value: text, setValue: setText } = useViewModelInstanceString('text', todoItem); - const { value: isDone, setValue: setIsDone } = useViewModelInstanceBoolean('isDone', todoItem); + const { value: text, setValue: setText } = useViewModelInstanceString( + 'text', + todoItem + ); + const { value: isDone, setValue: setIsDone } = useViewModelInstanceBoolean( + 'isDone', + todoItem + ); + + if (!todoItem) { + return
Item not found
; + } + + return ( +
+ setIsDone(e.target.checked)} + /> + setText(e.target.value)} + style={{ flex: 1 }} + /> +
+ Text: {text} +
+
+ Done: {isDone ? 'true' : 'false'} +
+
+ ); +}; - if (!todoItem) { - return
Item not found
; +export const TodoListTest = ({ src }: { src: string }) => { + const { rive, RiveComponent } = useRive({ + src, + autoplay: true, + artboard: 'Artboard', + autoBind: false, + stateMachines: 'State Machine 1', + }); + + const viewModel = useViewModel(rive, { name: 'TodoList' }); + const viewModelInstance = useViewModelInstance(viewModel, { rive }); + + const { + length, + addInstance, + addInstanceAt, + removeInstance, + removeInstanceAt, + getInstanceAt, + swap, + } = useViewModelInstanceList('items', viewModelInstance); + + const handleAddItem = () => { + const todoItemViewModel = rive?.viewModelByName?.('TodoItem'); + if (todoItemViewModel) { + const newTodoItem = todoItemViewModel.instance?.(); + if (newTodoItem) { + addInstance(newTodoItem); + } + } + }; + + const handleAddItemAt = () => { + const todoItemViewModel = rive?.viewModelByName?.('TodoItem'); + if (todoItemViewModel && length > 0) { + const newTodoItem = todoItemViewModel.instance?.(); + if (newTodoItem) { + addInstanceAt(newTodoItem, 1); + } } + }; - return ( -
- setIsDone(e.target.checked)} - /> - setText(e.target.value)} - style={{ flex: 1 }} - /> -
- Text: {text} -
-
- Done: {isDone ? 'true' : 'false'} -
-
- ); -}; + const handleRemoveFirstInstance = () => { + const firstInstance = getInstanceAt(0); + if (firstInstance) { + removeInstance(firstInstance); + } + }; -export const TodoListTest = ({ src }: { src: string }) => { - const { rive, RiveComponent } = useRive({ - src, - autoplay: true, - artboard: "Artboard", - autoBind: false, - stateMachines: "State Machine 1", - }); - - const viewModel = useViewModel(rive, { name: 'TodoList' }); - const viewModelInstance = useViewModelInstance(viewModel, { rive }); - - const { - length, - addInstance, - addInstanceAt, - removeInstance, - removeInstanceAt, - getInstanceAt, - swap - } = useViewModelInstanceList('items', viewModelInstance); - - const handleAddItem = () => { - const todoItemViewModel = rive?.viewModelByName?.('TodoItem'); - if (todoItemViewModel) { - const newTodoItem = todoItemViewModel.instance?.(); - if (newTodoItem) { - addInstance(newTodoItem); - } - } - }; - - const handleAddItemAt = () => { - const todoItemViewModel = rive?.viewModelByName?.('TodoItem'); - if (todoItemViewModel && length > 0) { - const newTodoItem = todoItemViewModel.instance?.(); - if (newTodoItem) { - addInstanceAt(newTodoItem, 1); - } - } - }; - - const handleRemoveFirstInstance = () => { - const firstInstance = getInstanceAt(0); - if (firstInstance) { - removeInstance(firstInstance); - } - }; - - const handleRemoveFirstByIndex = () => { - if (length > 0) { - removeInstanceAt(0); - } - }; - - const handleSwapItems = () => { - if (length >= 2) { - swap(0, 1); - } - }; - - return ( + const handleRemoveFirstByIndex = () => { + if (length > 0) { + removeInstanceAt(0); + } + }; + + const handleSwapItems = () => { + if (length >= 2) { + swap(0, 1); + } + }; + + return ( +
+ + {rive === null ? ( +
Loading…
+ ) : (
- - {rive === null ? ( -
Loading…
- ) : ( -
-
Items: {length}
- -
- - - - - - - - - -
- -
- {Array.from({ length }, (_, index) => ( - - ))} -
-
- )} +
Items: {length}
+ +
+ + + + + + + + + +
+ +
+ {Array.from({ length }, (_, index) => ( + + ))} +
- ); + )} +
+ ); }; export const ArtboardPropertyTest = ({ src }: { src: string }) => { - const [currentArtboard1, setCurrentArtboard1] = useState('None'); - const [currentArtboard2, setCurrentArtboard2] = useState('None'); - - const { rive, RiveComponent } = useRive({ - src, - autoplay: true, - artboard: "Main", - autoBind: true, - stateMachines: "State Machine 1", - }); - - const { setValue: setArtboard1 } = useViewModelInstanceArtboard('artboard_1', rive?.viewModelInstance); - const { setValue: setArtboard2 } = useViewModelInstanceArtboard('artboard_2', rive?.viewModelInstance); - - const handleSetArtboard1 = (artboardName: string) => { - if (rive) { - const artboard = rive.getArtboard(artboardName); - setArtboard1(artboard); - setCurrentArtboard1(artboardName); - } - }; - - const handleSetArtboard2 = (artboardName: string) => { - if (rive) { - const artboard = rive.getArtboard(artboardName); - setArtboard2(artboard); - setCurrentArtboard2(artboardName); - } - }; - - return ( + const [currentArtboard1, setCurrentArtboard1] = useState('None'); + const [currentArtboard2, setCurrentArtboard2] = useState('None'); + + const { rive, RiveComponent } = useRive({ + src, + autoplay: true, + artboard: 'Main', + autoBind: true, + stateMachines: 'State Machine 1', + }); + + const { setValue: setArtboard1 } = useViewModelInstanceArtboard( + 'artboard_1', + rive?.viewModelInstance + ); + const { setValue: setArtboard2 } = useViewModelInstanceArtboard( + 'artboard_2', + rive?.viewModelInstance + ); + + const handleSetArtboard1 = (artboardName: string) => { + if (rive) { + const artboard = rive.getArtboard(artboardName); + setArtboard1(artboard); + setCurrentArtboard1(artboardName); + } + }; + + const handleSetArtboard2 = (artboardName: string) => { + if (rive) { + const artboard = rive.getArtboard(artboardName); + setArtboard2(artboard); + setCurrentArtboard2(artboardName); + } + }; + + return ( +
+ + {rive === null ? ( +
Loading…
+ ) : (
- - {(rive === null) ?
Loading…
: ( -
-
-

Artboard 1:

-
- - - -
-
Current: {currentArtboard1}
-
- -
-

Artboard 2:

-
- - - -
-
Current: {currentArtboard2}
-
-
- )} +
+

Artboard 1:

+
+ + + +
+
+ Current: {currentArtboard1} +
+
+ +
+

Artboard 2:

+
+ + + +
+
+ Current: {currentArtboard2} +
+
- ); -}; \ No newline at end of file + )} +
+ ); +}; diff --git a/examples/src/components/Semantics.tsx b/examples/src/components/Semantics.tsx index 683d9a7..972389b 100644 --- a/examples/src/components/Semantics.tsx +++ b/examples/src/components/Semantics.tsx @@ -5,13 +5,7 @@ import React, { useRef, useState, } from 'react'; -import { - useRive, - EventType, - SemanticMode, - Fit, - Layout, -} from '@rive-app/react-canvas'; +import { useRive, SemanticMode, Fit, Layout } from '@rive-app/react-canvas'; /** * Testing semantics tree feature in Rive. Enable your accessibility tool to traverse the examples here (i.e. VoiceOver). @@ -22,10 +16,7 @@ import { // The example .riv files, copied into examples/public. Each is loaded with the // file's default artboard and a state machine named "State Machine 1". -const RIV_FILES = [ - 'data_binding_lists.riv', - 'semantic_warning_exp4.riv', -]; +const RIV_FILES = ['data_binding_lists.riv', 'semantic_warning_exp4.riv']; type FitValue = 'contain' | 'cover' | 'layout'; @@ -233,7 +224,7 @@ const Semantics = () => { onDrop={onDrop} > {source && ( -
+