Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/controlled-form/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './lib/ProviderAutocomplete';
export * from './lib/RadioGroup';
export * from './lib/Select';
export * from './lib/TextField';
export * from './lib/Timepicker';

export {
FormProvider,
Expand Down
104 changes: 104 additions & 0 deletions packages/controlled-form/src/lib/Timepicker.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { ControlledTimepicker, ControlledTimepickerProps } from './Timepicker';
import { Button } from '@availity/mui-button';
import { Paper } from '@availity/mui-paper';
import { Typography } from '@availity/mui-typography';
import { Grid } from '@availity/mui-layout';
import { AllControllerPropertiesCategorized, TimepickerPropsCategorized } from './Types';
import { FormProvider, useForm } from '..';
import dayjs, { Dayjs } from 'dayjs';

const meta: Meta<typeof ControlledTimepicker> = {
title: 'Form Components/Controlled Form/ControlledTimepicker',
component: ControlledTimepicker,
tags: ['autodocs'],
argTypes: { ...AllControllerPropertiesCategorized, ...TimepickerPropsCategorized },
};

export default meta;

export const _ControlledTimePicker: StoryObj<typeof ControlledTimepicker> = {
render: (args: ControlledTimepickerProps) => {
const methods = useForm();

return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit((data) => data)}>
<ControlledTimepicker {...args} />
<Grid container direction="row" justifyContent="space-between" marginTop={1}>
<Button
disabled={!methods?.formState?.isSubmitSuccessful}
children="Reset"
color="secondary"
onClick={() => methods.reset()}
/>
<Button type="submit" disabled={methods?.formState?.isSubmitSuccessful} children="Submit" />
</Grid>
{methods?.formState?.isSubmitSuccessful ? (
<Paper sx={{ padding: '1.5rem', marginTop: '1.5rem' }}>
<Typography variant="h2">Submitted Values</Typography>
<pre data-testid="result">{JSON.stringify(methods.getValues(), null, 2)}</pre>
</Paper>
) : null}
</form>
</FormProvider>
);
},
args: {
name: 'controlledTimepicker',
FieldProps: {
fullWidth: false,
helperText: 'Help text for the field',
helpTopicId: '1234',
label: 'Time',
},
},
};

/**
* In this example, the underlying value is stored as a string in the form values,
* but the timepicker always receives a Dayjs object. The transform prop is used to
* convert the value to and from the format you want to store in the underlying
* form values. You can see the underlying value when submitting the form.
*/
export const _Transform: StoryObj<typeof ControlledTimepicker> = {
render: (args: ControlledTimepickerProps) => {
const methods = useForm();

return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit((data) => data)}>
<ControlledTimepicker {...args} />
<Grid container direction="row" justifyContent="space-between" marginTop={1}>
<Button
disabled={!methods?.formState?.isSubmitSuccessful}
children="Reset"
color="secondary"
onClick={() => methods.reset()}
/>
<Button type="submit" disabled={methods?.formState?.isSubmitSuccessful} children="Submit" />
</Grid>
{methods?.formState?.isSubmitSuccessful ? (
<Paper sx={{ padding: '1.5rem', marginTop: '1.5rem' }}>
<Typography variant="h2">Submitted Values</Typography>
<pre data-testid="result">{JSON.stringify(methods.getValues(), null, 2)}</pre>
</Paper>
) : null}
</form>
</FormProvider>
);
},
args: {
transform: {
output: (value: Dayjs) => value?.format('hh:mm A'),
input: (value: string) => (value ? dayjs(value, 'hh:mm A') : null),
},
name: 'controlledTimepickerTransform',
FieldProps: {
fullWidth: false,
helperText: 'Help text for the field',
helpTopicId: '1234',
label: 'Time',
},
},
}
110 changes: 110 additions & 0 deletions packages/controlled-form/src/lib/Timepicker.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { render, fireEvent, waitFor } from '@testing-library/react';
import { ThemeProvider } from '@availity/theme-provider';
import dayjs from 'dayjs';
import { ControlledTimepicker } from './Timepicker';
import { TestForm } from './UtilComponents';

const onSubmit = jest.fn();

describe('ControlledTimepicker', () => {
test('should render successfully and submit selection', async () => {
const screen = render(
<ThemeProvider>
<TestForm UseFormOptions={{ values: { controlledTimepicker: null } }} onSubmit={onSubmit}>
<ControlledTimepicker
name="controlledTimepicker"
FieldProps={{
fullWidth: false,
helperText: 'Help text for the field',
helpTopicId: '1234',
label: 'Time',
}}
/>
</TestForm>
</ThemeProvider>
);
expect(screen.getAllByText('Time')).toBeTruthy();
const input = screen.getByLabelText('Choose time');
fireEvent.click(input);
const listboxes = screen.getAllByRole('listbox');
const hourOption = listboxes[0].querySelectorAll('[role="option"]');
fireEvent.click(hourOption[2]);

fireEvent.click(screen.getByText('OK'));
fireEvent.click(screen.getByText('Submit'));
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
const result = screen.getByTestId('result');
await waitFor(() => {
const controlledTimepickerValue = JSON.parse(result.innerHTML).controlledTimepicker;
expect(controlledTimepickerValue).toBeDefined();
expect(dayjs(controlledTimepickerValue).isValid()).toBeTruthy();
});
}, 10000);

describe('when using rules', () => {
describe('when required', () => {
test('should indicate it is required when passing a string', async () => {
const screen = render(
<ThemeProvider>
<TestForm UseFormOptions={{ values: { controlledTimepicker: null } }} onSubmit={onSubmit}>
<ControlledTimepicker
name="controlledTimepicker"
FieldProps={{
fullWidth: false,
helperText: 'Help text for the field',
helpTopicId: '1234',
label: 'Time',
}}
rules={{ required: 'This field is required' }}
/>
</TestForm>
</ThemeProvider>
);

expect(screen.getAllByText('*')).toBeDefined();
});

test('should indicate it is required when passing an object with true', async () => {
const screen = render(
<ThemeProvider>
<TestForm UseFormOptions={{ values: { controlledTimepicker: null } }} onSubmit={onSubmit}>
<ControlledTimepicker
name="controlledTimepicker"
FieldProps={{
fullWidth: false,
helperText: 'Help text for the field',
helpTopicId: '1234',
label: 'Time',
}}
rules={{ required: { value: true, message: 'This field is required' } }}
/>
</TestForm>
</ThemeProvider>
);

expect(screen.getAllByText('*')).toBeDefined();
});

test('should not indicate it is required when passing an object with false', async () => {
const screen = render(
<ThemeProvider>
<TestForm UseFormOptions={{ values: { controlledTimepicker: null } }} onSubmit={onSubmit}>
<ControlledTimepicker
name="controlledTimepicker"
FieldProps={{
fullWidth: false,
helperText: 'Help text for the field',
helpTopicId: '1234',
label: 'Time',
}}
rules={{ required: { value: false, message: 'This field is required' } }}
/>
</TestForm>
</ThemeProvider>
);

expect(screen.queryAllByText('*')).toHaveLength(0);
});
});
});
});
65 changes: 65 additions & 0 deletions packages/controlled-form/src/lib/Timepicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Timepicker, TimepickerProps } from '@availity/mui-datepicker';
import { RegisterOptions, FieldValues, Controller } from 'react-hook-form';
import { ControllerProps, TransformProp } from './Types';
import { Dayjs } from 'dayjs';

export type ControlledTimepickerProps<Output = Dayjs | null> = Omit<
TimepickerProps,
'onBlur' | 'onChange' | 'value' | 'name'
> &
Pick<RegisterOptions<FieldValues, string>, 'onBlur' | 'onChange' | 'value'> &
ControllerProps &
TransformProp<Dayjs | null, Output>;

export const ControlledTimepicker = <Output = Dayjs | null,>({
name,
defaultValue,
onBlur,
onChange,
rules = {},
shouldUnregister,
value,
FieldProps,
transform,
...rest
}: ControlledTimepickerProps<Output>) => {
return (
<Controller
name={name}
defaultValue={defaultValue}
rules={{
onBlur,
onChange,
shouldUnregister,
value,
...rules,
}}
shouldUnregister={shouldUnregister}
render={({ field: { onChange, value, onBlur, ref }, fieldState: { error } }) => (
<Timepicker
{...rest}
FieldProps={{
...FieldProps,
required: (typeof rules.required === 'object' ? rules.required.value : !!rules.required) || FieldProps?.required,
error: !!error,
helperText: error ? (
<>
{error.message}
<br />
{FieldProps?.helperText}
</>
) : (
FieldProps?.helperText
),
inputRef: ref,
inputProps: {
onBlur: onBlur,
},
}}
onChange={(e) => onChange(transform?.output?.(e) ?? e)}
value={transform?.input?.(value) || value || null}
/>
)}
/>
);
};
52 changes: 52 additions & 0 deletions packages/controlled-form/src/lib/Types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ControlledRadioGroupProps,
ControlledSelectProps,
ControlledTextFieldProps,
ControlledTimepickerProps,
} from '..';
import { HTMLAttributes } from 'react';

Expand Down Expand Up @@ -74,6 +75,14 @@ type DatepickerPropsObject = Record<
{ table: { category: 'Input Props' } }
>;

type TimepickerPropsObject = Record<
keyof Omit<
ControlledTimepickerProps,
keyof AllControllerProps | keyof Omit<HTMLAttributes<undefined>, 'autoFocus' | 'className' | 'onError'>
>,
{ table: { category: 'Input Props' } }
>;

type ProviderAutocompletePropsObject = Record<
keyof Omit<
ControlledProviderAutocompleteProps,
Expand Down Expand Up @@ -429,6 +438,49 @@ export const DatepickerPropsCategorized: DatepickerPropsObject = {
clearable: { table: { category: 'Input Props' } },
};

export const TimepickerPropsCategorized: TimepickerPropsObject = {
autoFocus: { table: { category: 'Input Props' } },
className: { table: { category: 'Input Props' } },
onError: { table: { category: 'Input Props' } },
sx: { table: { category: 'Input Props' } },
label: { table: { category: 'Input Props' } },
view: { table: { category: 'Input Props' } },
readOnly: { table: { category: 'Input Props' } },
onClose: { table: { category: 'Input Props' } },
onOpen: { table: { category: 'Input Props' } },
open: { table: { category: 'Input Props' } },
FieldProps: { table: { category: 'Input Props' } },
disableFuture: { table: { category: 'Input Props' } },
disablePast: { table: { category: 'Input Props' } },
views: { table: { category: 'Input Props' } },
onViewChange: { table: { category: 'Input Props' } },
localeText: { table: { category: 'Input Props' } },
onAccept: { table: { category: 'Input Props' } },
viewRenderers: { table: { category: 'Input Props' } },
referenceDate: { table: { category: 'Input Props' } },
timezone: { table: { category: 'Input Props' } },
formatDensity: { table: { category: 'Input Props' } },
enableAccessibleFieldDOMStructure: { table: { category: 'Input Props' } },
selectedSections: { table: { category: 'Input Props' } },
onSelectedSectionsChange: { table: { category: 'Input Props' } },
closeOnSelect: { table: { category: 'Input Props' } },
format: { table: { category: 'Input Props' } },
inputRef: { table: { category: 'Input Props' } },
placement: { table: { category: 'Input Props' } },
clearable: { table: { category: 'Input Props' } },
onClear: { table: { category: 'Input Props' } },
minutesStep: { table: { category: 'Input Props' } },
minTime: { table: { category: 'Input Props' } },
maxTime: { table: { category: 'Input Props' } },
shouldDisableTime: { table: { category: 'Input Props' } },
disableIgnoringDatePartForTimeValidation: { table: { category: 'Input Props' } },
ampm: { table: { category: 'Input Props' } },
skipDisabled: { table: { category: 'Input Props' } },
timeSteps: { table: { category: 'Input Props' } },
ampmInClock: { table: { category: 'Input Props' } },
thresholdToRenderTimeInASingleColumn: { table: { category: 'Input Props' } }
};

export const CodesAutocompletePropsCategorized: CodesAutocompletePropsObject = {
classes: { table: { category: 'Input Props' } },
id: { table: { category: 'Input Props' } },
Expand Down
1 change: 1 addition & 0 deletions packages/datepicker/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './lib/Datepicker';
export * from './lib/Timepicker';
Loading
Loading