Skip to content

Commit

Permalink
fix(uniform): refactor form debug state handling
Browse files Browse the repository at this point in the history
  • Loading branch information
toxsick committed Nov 6, 2024
1 parent d49db4b commit 3a175a1
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 61 deletions.
23 changes: 22 additions & 1 deletion packages/uniform/src/Form/subcomponents/FormContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { FieldValues, SubmitHandler } from 'react-hook-form';
import React, { useMemo, useState } from 'react';
import { FormProvider as HookFormProvider, useForm } from 'react-hook-form';

import { useLocalStorage } from '@fuf-stack/pixels';

/**
* recursively removes all fields that are null or undefined before
* the form data is passed to the veto validation function
Expand All @@ -17,6 +19,8 @@ export const removeNullishFields = (obj: Record<string, unknown>) => {
);
};

type DebugMode = 'debug' | 'debug-testids' | 'off';

/**
* The `UniformContext` provides control over the form's submission behavior and may optionally include
* a Veto validation schema for form validation.
Expand All @@ -31,12 +35,18 @@ export const removeNullishFields = (obj: Record<string, unknown>) => {
* or access the validation schema for managing form validation logic.
*/
export const UniformContext = React.createContext<{
/** Form debug mode enabled or not */
debugMode: DebugMode;
/** Function to update if the form can currently be submitted */
preventSubmit: (prevent: boolean) => void;
/** Setter to enable or disable form debug mode */
setDebugMode: (debugMode: DebugMode) => void;
/** Optional Veto validation schema instance for form validation */
validation?: VetoInstance;
}>({
debugMode: 'off',
preventSubmit: () => undefined,
setDebugMode: () => undefined,
validation: undefined,
});

Expand All @@ -56,10 +66,13 @@ interface FormProviderProps {
validationTrigger: 'onChange' | 'onBlur' | 'onSubmit' | 'onTouched' | 'all';
}

const LOCALSTORAGE_DEBUG_MODE_KEY = 'uniform:debug-mode';

/**
* FormProvider component provides:
* - The veto validation schema context
* - Submit handler creation and submission control with preventSubmit
* - Form Debug Mode state
* - React Hook Form context
*/
const FormProvider: React.FC<FormProviderProps> = ({
Expand All @@ -72,16 +85,24 @@ const FormProvider: React.FC<FormProviderProps> = ({
// Control if the form can currently be submitted
const [preventSubmit, setPreventSubmit] = useState(false);

// Form Debug mode state is handled in the form context
const [debugMode, setDebugMode] = useLocalStorage<DebugMode>(
LOCALSTORAGE_DEBUG_MODE_KEY,
'off',
);

// Memoize the context value to prevent re-renders
const contextValue = useMemo(
() => ({
debugMode,
preventSubmit: (prevent: boolean) => {
setPreventSubmit(prevent);
},
setDebugMode,
validation,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
[debugMode],
);

// Initialize react hook form
Expand Down
30 changes: 13 additions & 17 deletions packages/uniform/src/Form/subcomponents/FormDebugViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { FaTimes } from 'react-icons/fa';
import { FaBug, FaBullseye } from 'react-icons/fa6';

import { cn } from '@fuf-stack/pixel-utils';
import { Button, Card, Json, useLocalStorage } from '@fuf-stack/pixels';
import { Button, Card, Json } from '@fuf-stack/pixels';

import { useFormContext } from '../../hooks';

Expand All @@ -14,28 +14,22 @@ interface FormDebugViewerProps {
className?: string;
}

const LOCALSTORAGE_DEBUG_KEY = 'uniform:form-debug-enabled';
const LOCALSTORAGE_COPY_TEST_ID_KEY = 'uniform:form-debug-copy-test-id-enabled';

/** Renders a form debug panel with information about the current form state */
const FormDebugViewer = ({ className = undefined }: FormDebugViewerProps) => {
const {
watch,
debugMode,
formState: { dirtyFields, isValid, isSubmitting },
setDebugMode,
validation,
watch,
} = useFormContext();

const [debug, setDebug] = useLocalStorage(LOCALSTORAGE_DEBUG_KEY, false);
const [copyTestId, setCopyTestId] = useLocalStorage(
LOCALSTORAGE_COPY_TEST_ID_KEY,
false,
);

const [validationErrors, setValidationErrors] = useState<
VetoError['errors'] | null
>(null);

const formValues = watch();
const debugTestIdsEnabled = debugMode === 'debug-testids';

useEffect(
() => {
Expand All @@ -51,11 +45,11 @@ const FormDebugViewer = ({ className = undefined }: FormDebugViewerProps) => {
[JSON.stringify(formValues)],
);

if (!debug) {
if (!debugMode || debugMode === 'off') {
return (
<Button
ariaLabel="Enable form debug mode"
onClick={() => setDebug(!debug)}
onClick={() => setDebugMode('debug')}
className="absolute bottom-2.5 right-2.5 w-5 text-default-400"
variant="light"
icon={<FaBug />}
Expand All @@ -71,20 +65,22 @@ const FormDebugViewer = ({ className = undefined }: FormDebugViewerProps) => {
<span className="text-lg">Debug Mode</span>
<Button
icon={<FaTimes className="text-danger" />}
onClick={() => setDebug(false)}
onClick={() => setDebugMode('off')}
size="sm"
variant="flat"
/>
</div>
}
>
<Button
variant={copyTestId ? 'solid' : 'light'}
variant={debugTestIdsEnabled ? 'solid' : 'light'}
icon={<FaBullseye />}
className="mb-4 ml-auto mr-auto"
onClick={() => setCopyTestId(!copyTestId)}
onClick={() =>
setDebugMode(debugMode === 'debug' ? 'debug-testids' : 'debug')
}
>
{copyTestId ? 'Hide CopyButton' : 'Show CopyButton'}
{debugTestIdsEnabled ? 'Hide CopyButton' : 'Show CopyButton'}
</Button>
<Json
value={{
Expand Down
17 changes: 12 additions & 5 deletions packages/uniform/src/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ const Input = ({
testId: _testId = undefined,
type = undefined,
}: InputProps) => {
const { control, getFieldState } = useFormContext();
const { control, debugMode, getFieldState } = useFormContext();
const { error, invalid, required, testId } = getFieldState(name, _testId);

const showTestIdCopyButton = debugMode === 'debug-testids';
const showLabel = label || showTestIdCopyButton;

return (
<Controller
control={control}
Expand All @@ -69,10 +72,14 @@ const Input = ({
isInvalid={invalid}
isRequired={required}
label={
<>
{label}
<FieldCopyTestIdButton testId={testId} />
</>
showLabel && (
<>
{label}
{showTestIdCopyButton && (
<FieldCopyTestIdButton testId={testId} />
)}
</>
)
}
labelPlacement="outside"
name={name}
Expand Down
27 changes: 0 additions & 27 deletions packages/uniform/src/Input/__snapshots__/Input.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ exports[`Story Snapshots > Default 1`] = `
class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)]"
data-filled="true"
data-filled-within="true"
data-has-elements="true"
data-has-label="true"
data-slot="base"
>
<div
Expand All @@ -26,20 +24,13 @@ exports[`Story Snapshots > Default 1`] = `
data-slot="input-wrapper"
style="cursor: text;"
>
<label
class="absolute pointer-events-none origin-top-left rtl:origin-top-right subpixel-antialiased block text-foreground-500 will-change-auto !duration-200 !ease-out motion-reduce:transition-none transition-[transform,color,left,opacity] group-data-[filled-within=true]:text-foreground group-data-[filled-within=true]:pointer-events-auto pb-0 z-20 top-1/2 -translate-y-1/2 group-data-[filled-within=true]:start-0 start-3 end-auto text-small group-data-[filled-within=true]:-translate-y-[calc(100%_+_theme(fontSize.small)/2_+_20px)] pe-2 max-w-full text-ellipsis overflow-hidden"
data-slot="label"
for="react-aria-react-useId-mock"
id="react-aria-react-useId-mock"
/>
<div
class="inline-flex w-full items-center h-full box-border"
data-slot="inner-wrapper"
>
<input
aria-describedby="react-aria-react-useId-mock react-aria-react-useId-mock"
aria-label=" "
aria-labelledby="react-aria-react-useId-mock"
class="w-full font-normal bg-transparent !outline-none placeholder:text-foreground-500 focus-visible:outline-none data-[has-start-content=true]:ps-1.5 data-[has-end-content=true]:pe-1.5 file:cursor-pointer file:bg-transparent file:border-0 autofill:bg-transparent bg-clip-text text-small"
data-slot="input"
data-testid="inputfield"
Expand Down Expand Up @@ -467,8 +458,6 @@ exports[`Story Snapshots > WithInitialValue 1`] = `
class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)] is-filled"
data-filled="true"
data-filled-within="true"
data-has-elements="true"
data-has-label="true"
data-has-value="true"
data-slot="base"
>
Expand All @@ -481,20 +470,13 @@ exports[`Story Snapshots > WithInitialValue 1`] = `
data-slot="input-wrapper"
style="cursor: text;"
>
<label
class="absolute pointer-events-none origin-top-left rtl:origin-top-right subpixel-antialiased block text-foreground-500 will-change-auto !duration-200 !ease-out motion-reduce:transition-none transition-[transform,color,left,opacity] group-data-[filled-within=true]:text-foreground group-data-[filled-within=true]:pointer-events-auto pb-0 z-20 top-1/2 -translate-y-1/2 group-data-[filled-within=true]:start-0 start-3 end-auto text-small group-data-[filled-within=true]:-translate-y-[calc(100%_+_theme(fontSize.small)/2_+_20px)] pe-2 max-w-full text-ellipsis overflow-hidden"
data-slot="label"
for="react-aria-react-useId-mock"
id="react-aria-react-useId-mock"
/>
<div
class="inline-flex w-full items-center h-full box-border"
data-slot="inner-wrapper"
>
<input
aria-describedby="react-aria-react-useId-mock react-aria-react-useId-mock"
aria-label=" "
aria-labelledby="react-aria-react-useId-mock"
class="w-full font-normal bg-transparent !outline-none placeholder:text-foreground-500 focus-visible:outline-none data-[has-start-content=true]:ps-1.5 data-[has-end-content=true]:pe-1.5 file:cursor-pointer file:bg-transparent file:border-0 autofill:bg-transparent bg-clip-text text-small is-filled"
data-filled="true"
data-filled-within="true"
Expand Down Expand Up @@ -648,8 +630,6 @@ exports[`Story Snapshots > WithSelect 1`] = `
class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)]"
data-filled="true"
data-filled-within="true"
data-has-elements="true"
data-has-label="true"
data-slot="base"
>
<div
Expand All @@ -661,12 +641,6 @@ exports[`Story Snapshots > WithSelect 1`] = `
data-slot="input-wrapper"
style="cursor: text;"
>
<label
class="absolute pointer-events-none origin-top-left rtl:origin-top-right subpixel-antialiased block text-foreground-500 will-change-auto !duration-200 !ease-out motion-reduce:transition-none transition-[transform,color,left,opacity] group-data-[filled-within=true]:text-foreground group-data-[filled-within=true]:pointer-events-auto pb-0 z-20 top-1/2 -translate-y-1/2 group-data-[filled-within=true]:start-0 start-3 end-auto text-small group-data-[filled-within=true]:-translate-y-[calc(100%_+_theme(fontSize.small)/2_+_20px)] pe-2 max-w-full text-ellipsis overflow-hidden"
data-slot="label"
for="react-aria-react-useId-mock"
id="react-aria-react-useId-mock"
/>
<div
class="inline-flex w-full items-center h-full box-border"
data-slot="inner-wrapper"
Expand All @@ -687,7 +661,6 @@ exports[`Story Snapshots > WithSelect 1`] = `
<input
aria-describedby="react-aria-react-useId-mock react-aria-react-useId-mock"
aria-label=" "
aria-labelledby="react-aria-react-useId-mock"
class="w-full font-normal bg-transparent !outline-none placeholder:text-foreground-500 focus-visible:outline-none data-[has-start-content=true]:ps-1.5 data-[has-end-content=true]:pe-1.5 file:cursor-pointer file:bg-transparent file:border-0 autofill:bg-transparent bg-clip-text text-small"
data-has-end-content="true"
data-has-start-content="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,23 @@
import { FaBullseye } from 'react-icons/fa6';

import { cn } from '@fuf-stack/pixel-utils';
import { Button, useLocalStorage } from '@fuf-stack/pixels';
import { Button } from '@fuf-stack/pixels';

export interface FieldCopyTestIdButtonProps {
className?: string;
testId: string;
}

const LOCALSTORAGE_DEBUG_KEY = 'uniform:form-debug-enabled';
const LOCALSTORAGE_COPY_TEST_ID_KEY = 'uniform:form-debug-copy-test-id-enabled';

const FieldCopyTestIdButton = ({
className = undefined,
testId,
}: FieldCopyTestIdButtonProps) => {
const [debug] = useLocalStorage(LOCALSTORAGE_DEBUG_KEY, false);
const [copyTestId] = useLocalStorage(LOCALSTORAGE_COPY_TEST_ID_KEY, false);

const copyToClipboard = () => {
navigator.clipboard.writeText(testId).catch((err) => {
console.error('Error copying TestId to clipboard', err);
});
};

if (!debug || !copyTestId) {
return null;
}

return (
<Button
className={cn(className, 'pointer-events-auto')}
Expand Down

0 comments on commit 3a175a1

Please sign in to comment.