Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions apps/docs/src/remix-hook-form/autofill-css-animation.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof formSchema>;

// Custom TextField that uses the useAutofillAnimationDetection hook
const AutofillDetectingTextField = (props: React.ComponentProps<typeof TextField>) => {
const inputRef = useRef<HTMLInputElement>(null);
const { isAutofilled } = useAutofillAnimationDetection(inputRef);

return (
<div className="relative">
<TextField {...props} ref={inputRef} />
{isAutofilled && (
<div className="absolute right-0 top-0 bg-green-100 text-green-800 text-xs px-2 py-1 rounded">
Autofilled
</div>
)}
</div>
);
};

const AutofillCssAnimationExample = () => {
const fetcher = useFetcher<{ message: string }>();
const methods = useRemixForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
email: '',
phone: '',
address: '',
},
fetcher,
submitConfig: {
action: '/',
method: 'post',
},
});

return (
<RemixFormProvider {...methods}>
<fetcher.Form onSubmit={methods.handleSubmit}>
<div className="space-y-4 max-w-md mx-auto">
<h2 className="text-xl font-bold mb-4">Autofill Detection with CSS Animation</h2>
<p className="text-sm text-gray-500 mb-6">
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.
</p>

<div className="space-y-4">
<AutofillDetectingTextField
name="name"
label="Full Name"
autoComplete="name"
/>

<AutofillDetectingTextField
name="email"
label="Email Address"
type="email"
autoComplete="email"
/>

<AutofillDetectingTextField
name="phone"
label="Phone Number"
type="tel"
autoComplete="tel"
/>

<AutofillDetectingTextField
name="address"
label="Street Address"
autoComplete="street-address"
/>

<Button type="submit" className="w-full mt-6">
Submit
</Button>

{fetcher.data?.message && (
<p className="mt-2 text-green-600">{fetcher.data.message}</p>
)}
</div>
</div>
</fetcher.Form>
</RemixFormProvider>
);
};

const handleFormSubmission = async (request: Request) => {
const { data, errors } = await getValidatedFormData<FormData>(request, zodResolver(formSchema));

if (errors) {
return { errors };
}

return { message: 'Form submitted successfully' };
};

const meta: Meta<typeof TextField> = {
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<typeof meta>;

export const CssAnimationExample: Story = {
decorators: [
withReactRouterStubDecorator({
routes: [
{
path: '/',
Component: AutofillCssAnimationExample,
action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request),
},
],
}),
],
};

2 changes: 2 additions & 0 deletions packages/components/src/ui/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './use-autofill-animation-detection';

102 changes: 102 additions & 0 deletions packages/components/src/ui/hooks/use-autofill-animation-detection.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement | HTMLTextAreaElement>
) {
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 };
}