diff --git a/apps/docs/src/remix-hook-form/autofill-css-animation.stories.tsx b/apps/docs/src/remix-hook-form/autofill-css-animation.stories.tsx new file mode 100644 index 00000000..40c3a1a9 --- /dev/null +++ b/apps/docs/src/remix-hook-form/autofill-css-animation.stories.tsx @@ -0,0 +1,146 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { TextField } from '@lambdacurry/forms/remix-hook-form/text-field'; +import { Button } from '@lambdacurry/forms/ui/button'; +import { useAutofillAnimationDetection } from '@lambdacurry/forms/ui/hooks/use-autofill-animation-detection'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { useRef } from 'react'; +import { type ActionFunctionArgs, useFetcher } from 'react-router'; +import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form'; +import { z } from 'zod'; +import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub'; + +const formSchema = z.object({ + name: z.string().min(1, 'Name is required'), + email: z.string().email('Invalid email address'), + phone: z.string().min(1, 'Phone number is required'), + address: z.string().min(1, 'Address is required'), +}); + +type FormData = z.infer; + +// Custom TextField that uses the useAutofillAnimationDetection hook +const AutofillDetectingTextField = (props: React.ComponentProps) => { + const inputRef = useRef(null); + const { isAutofilled } = useAutofillAnimationDetection(inputRef); + + return ( +
+ + {isAutofilled && ( +
+ Autofilled +
+ )} +
+ ); +}; + +const AutofillCssAnimationExample = () => { + const fetcher = useFetcher<{ message: string }>(); + const methods = useRemixForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: '', + email: '', + phone: '', + address: '', + }, + fetcher, + submitConfig: { + action: '/', + method: 'post', + }, + }); + + return ( + + +
+

Autofill Detection with CSS Animation

+

+ This form demonstrates autofill detection using CSS animations and the animationstart event. + Try using your browser's autofill feature to populate these fields and watch for the "Autofilled" indicator. +

+ +
+ + + + + + + + + + + {fetcher.data?.message && ( +

{fetcher.data.message}

+ )} +
+
+
+
+ ); +}; + +const handleFormSubmission = async (request: Request) => { + const { data, errors } = await getValidatedFormData(request, zodResolver(formSchema)); + + if (errors) { + return { errors }; + } + + return { message: 'Form submitted successfully' }; +}; + +const meta: Meta = { + title: 'RemixHookForm/AutofillCssAnimation', + component: TextField, + parameters: { + layout: 'centered', + docs: { + description: { + component: 'Demonstrates autofill detection using CSS animations and the animationstart event.' + } + } + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const CssAnimationExample: Story = { + decorators: [ + withReactRouterStubDecorator({ + routes: [ + { + path: '/', + Component: AutofillCssAnimationExample, + action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request), + }, + ], + }), + ], +}; + diff --git a/packages/components/src/ui/hooks/index.ts b/packages/components/src/ui/hooks/index.ts new file mode 100644 index 00000000..e672fbc2 --- /dev/null +++ b/packages/components/src/ui/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './use-autofill-animation-detection'; + diff --git a/packages/components/src/ui/hooks/use-autofill-animation-detection.ts b/packages/components/src/ui/hooks/use-autofill-animation-detection.ts new file mode 100644 index 00000000..0167801f --- /dev/null +++ b/packages/components/src/ui/hooks/use-autofill-animation-detection.ts @@ -0,0 +1,102 @@ +import { useEffect, useState } from 'react'; + +/** + * Hook to detect browser autofill using CSS animation events. + * This technique works by applying a CSS animation to autofilled inputs + * and listening for the animationstart event. + * + * @param inputRef - Reference to the input element + * @returns Object containing isAutofilled state and reset function + */ +export function useAutofillAnimationDetection( + inputRef: React.RefObject +) { + const [isAutofilled, setIsAutofilled] = useState(false); + + useEffect(() => { + const element = inputRef.current; + if (!element) return; + + // Function to handle animation events + const handleAnimation = (event: AnimationEvent) => { + // Check if the animation name matches our autofill detection animations + if ( + event.animationName === 'onAutoFillStart' || + event.animationName === 'onAutoFill' + ) { + setIsAutofilled(true); + } else if ( + event.animationName === 'onAutoFillCancel' || + event.animationName === 'onAutoFillOut' + ) { + setIsAutofilled(false); + } + }; + + // Add event listener for animation events + element.addEventListener('animationstart', handleAnimation as EventListener); + + // Add the CSS for animation detection + const style = document.createElement('style'); + style.textContent = ` + /* Chrome, Safari, Edge */ + @keyframes onAutoFill { + from {} + to {} + } + + @keyframes onAutoFillCancel { + from {} + to {} + } + + input:-webkit-autofill { + animation-name: onAutoFill; + animation-fill-mode: both; + } + + input:not(:-webkit-autofill) { + animation-name: onAutoFillCancel; + animation-fill-mode: both; + } + + /* Firefox */ + @keyframes onAutoFillStart { + from {} + to {} + } + + @keyframes onAutoFillOut { + from {} + to {} + } + + /* Firefox doesn't have a specific pseudo-class for autofill, + but we can detect it by checking for the specific background color it applies */ + input:-moz-autofill { + animation-name: onAutoFillStart; + animation-fill-mode: both; + } + + input:not(:-moz-autofill) { + animation-name: onAutoFillOut; + animation-fill-mode: both; + } + `; + document.head.appendChild(style); + + // Clean up + return () => { + element.removeEventListener('animationstart', handleAnimation as EventListener); + document.head.removeChild(style); + }; + }, [inputRef]); + + // Function to reset the autofilled state + const resetAutofilled = () => { + setIsAutofilled(false); + }; + + return { isAutofilled, resetAutofilled }; +} +