diff --git a/.changeset/fix-appproxy-react-router-forwardref.md b/.changeset/fix-appproxy-react-router-forwardref.md new file mode 100644 index 0000000000..eff52380fb --- /dev/null +++ b/.changeset/fix-appproxy-react-router-forwardref.md @@ -0,0 +1,6 @@ +--- +'@shopify/shopify-app-react-router': patch +--- + +`AppProxyLink` now uses `forwardRef`, allowing consumers to attach a ref to the +underlying `` element (e.g. `anchor.current.focus()`). diff --git a/.changeset/fix-appproxy-remix-forwardref.md b/.changeset/fix-appproxy-remix-forwardref.md new file mode 100644 index 0000000000..50db94184b --- /dev/null +++ b/.changeset/fix-appproxy-remix-forwardref.md @@ -0,0 +1,6 @@ +--- +'@shopify/shopify-app-remix': patch +--- + +`AppProxyForm` and `AppProxyLink` now use `forwardRef`, allowing consumers to +attach a ref to the underlying DOM element (e.g. `form.current.submit()`). diff --git a/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/AppProxyLink.tsx b/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/AppProxyLink.tsx index 3e743877c0..090fc47e26 100644 --- a/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/AppProxyLink.tsx +++ b/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/AppProxyLink.tsx @@ -1,4 +1,4 @@ -import {useContext} from 'react'; +import {forwardRef, useContext} from 'react'; import {AppProxyProviderContext} from '../AppProxyProvider'; @@ -39,20 +39,22 @@ export interface AppProxyLinkProps extends React.DetailedHTMLProps< * } * ``` */ -export function AppProxyLink(props: AppProxyLinkProps) { - const context = useContext(AppProxyProviderContext); +export const AppProxyLink = forwardRef( + function AppProxyLink(props, ref) { + const context = useContext(AppProxyProviderContext); - if (!context) { - throw new Error( - 'AppProxyLink must be used within an AppProxyProvider component', - ); - } + if (!context) { + throw new Error( + 'AppProxyLink must be used within an AppProxyProvider component', + ); + } - const {children, href, ...otherProps} = props; + const {children, href, ...otherProps} = props; - return ( - - {children} - - ); -} + return ( + + {children} + + ); + }, +); diff --git a/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx b/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx index dbb47271b0..61440758aa 100644 --- a/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx +++ b/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx @@ -1,3 +1,4 @@ +import {createRef} from 'react'; import {mount} from '@shopify/react-testing'; import '../../../__tests__/test-helper'; @@ -45,4 +46,21 @@ describe('', () => { }); expect(component).toContainReactHtml('Hello world'); }); + + it('forwards ref to the underlying anchor element', () => { + // GIVEN + const ref = createRef(); + + // WHEN + mount( + + + Hello world + + , + ); + + // THEN + expect(ref.current).toBeInstanceOf(HTMLAnchorElement); + }); }); diff --git a/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/AppProxyForm.tsx b/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/AppProxyForm.tsx index ca69c36ceb..8547d4279a 100644 --- a/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/AppProxyForm.tsx +++ b/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/AppProxyForm.tsx @@ -1,4 +1,4 @@ -import {useContext} from 'react'; +import {forwardRef, useContext} from 'react'; import {Form, type FormProps} from '@remix-run/react'; import {AppProxyProviderContext} from '../AppProxyProvider'; @@ -63,20 +63,22 @@ export interface AppProxyFormProps extends FormProps { * } * ``` */ -export function AppProxyForm(props: AppProxyFormProps) { - const context = useContext(AppProxyProviderContext); +export const AppProxyForm = forwardRef( + function AppProxyForm(props, ref) { + const context = useContext(AppProxyProviderContext); - if (!context) { - throw new Error( - 'AppProxyForm must be used within an AppProxyProvider component', - ); - } + if (!context) { + throw new Error( + 'AppProxyForm must be used within an AppProxyProvider component', + ); + } - const {children, action, ...otherProps} = props; + const {children, action, ...otherProps} = props; - return ( -
- {children} -
- ); -} + return ( +
+ {children} +
+ ); + }, +); diff --git a/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/__tests__/AppProxyForm.test.tsx b/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/__tests__/AppProxyForm.test.tsx index 7df0149c28..d28b878b0a 100644 --- a/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/__tests__/AppProxyForm.test.tsx +++ b/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/__tests__/AppProxyForm.test.tsx @@ -1,3 +1,4 @@ +import {createRef} from 'react'; import {mount} from '@shopify/react-testing'; import {createRemixStub} from '@remix-run/testing'; @@ -52,4 +53,26 @@ describe('', () => { expect(component).toContainReactComponent('form', {action: '/my-action/'}); expect(component).toContainReactHtml('Hello world'); }); + + it('forwards ref to the underlying form element', () => { + // GIVEN + const ref = createRef(); + + function MyComponent() { + return ( + + + Hello world + + + ); + } + + // WHEN + const RemixStub = createRemixStub([{path: '/', Component: MyComponent}]); + mount(); + + // THEN + expect(ref.current).toBeInstanceOf(HTMLFormElement); + }); }); diff --git a/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/AppProxyLink.tsx b/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/AppProxyLink.tsx index 76b7403529..c7c2e3f351 100644 --- a/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/AppProxyLink.tsx +++ b/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/AppProxyLink.tsx @@ -1,4 +1,4 @@ -import {useContext} from 'react'; +import {forwardRef, useContext} from 'react'; import {AppProxyProviderContext} from '../AppProxyProvider'; @@ -39,20 +39,22 @@ export interface AppProxyLinkProps extends React.DetailedHTMLProps< * } * ``` */ -export function AppProxyLink(props: AppProxyLinkProps) { - const context = useContext(AppProxyProviderContext); +export const AppProxyLink = forwardRef( + function AppProxyLink(props, ref) { + const context = useContext(AppProxyProviderContext); - if (!context) { - throw new Error( - 'AppProxyLink must be used within an AppProxyProvider component', - ); - } + if (!context) { + throw new Error( + 'AppProxyLink must be used within an AppProxyProvider component', + ); + } - const {children, href, ...otherProps} = props; + const {children, href, ...otherProps} = props; - return ( - - {children} - - ); -} + return ( + + {children} + + ); + }, +); diff --git a/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx b/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx index dbb47271b0..61440758aa 100644 --- a/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx +++ b/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx @@ -1,3 +1,4 @@ +import {createRef} from 'react'; import {mount} from '@shopify/react-testing'; import '../../../__tests__/test-helper'; @@ -45,4 +46,21 @@ describe('', () => { }); expect(component).toContainReactHtml('Hello world'); }); + + it('forwards ref to the underlying anchor element', () => { + // GIVEN + const ref = createRef(); + + // WHEN + mount( + + + Hello world + + , + ); + + // THEN + expect(ref.current).toBeInstanceOf(HTMLAnchorElement); + }); });