From fc31230a891eab4df78dec725c532f902d8ab26a Mon Sep 17 00:00:00 2001 From: trevor-anderson Date: Tue, 20 Feb 2024 11:27:48 -0500 Subject: [PATCH] feat: mv all Form inputs into comps/Form/inputs/ --- src/components/Form/AutoComplete.tsx | 207 ------------- src/components/Form/AutoCompleteContact.tsx | 89 ------ src/components/Form/AutoCompleteStates.tsx | 75 ----- src/components/Form/AutoCompleteWorkOrder.tsx | 104 ------- src/components/Form/CurrencyInput.tsx | 78 ----- src/components/Form/DatePicker.tsx | 56 ---- src/components/Form/DateTimePicker.tsx | 48 --- src/components/Form/PhoneInput.tsx | 47 --- src/components/Form/Select.tsx | 72 ----- src/components/Form/TextInput.tsx | 40 --- src/components/Form/inputs/AutoComplete.tsx | 284 ++++++++++++++++++ .../Form/inputs/AutoCompleteContact.tsx | 47 +++ .../Form/inputs/AutoCompleteMyContacts.tsx | 41 +++ .../Form/inputs/AutoCompleteStates.tsx | 55 ++++ .../Form/inputs/AutoCompleteWorkOrder.tsx | 76 +++++ .../inputs/AutoCompleteWorkOrderCategory.tsx | 51 ++++ src/components/Form/inputs/BaseTextField.tsx | 34 +++ src/components/Form/inputs/CurrencyInput.tsx | 83 +++++ src/components/Form/inputs/DatePicker.tsx | 118 ++++++++ src/components/Form/inputs/DateTimePicker.tsx | 121 ++++++++ .../Form/{ => inputs}/PasswordInput.tsx | 18 +- src/components/Form/inputs/PhoneInput.tsx | 49 +++ src/components/Form/inputs/RegionInput.tsx | 30 ++ src/components/Form/inputs/Select.tsx | 78 +++++ src/components/Form/{ => inputs}/Slider.tsx | 44 +-- .../Form/inputs/SliderWorkOrderPriority.tsx | 55 ++++ src/components/Form/inputs/TextInput.tsx | 88 ++++++ src/components/Form/inputs/index.ts | 18 ++ src/components/Form/types.ts | 36 --- .../Form/useDefaultTextFieldVariant.ts | 13 - src/components/Form/useFormikFieldProps.ts | 82 ----- 31 files changed, 1263 insertions(+), 974 deletions(-) delete mode 100644 src/components/Form/AutoComplete.tsx delete mode 100644 src/components/Form/AutoCompleteContact.tsx delete mode 100644 src/components/Form/AutoCompleteStates.tsx delete mode 100644 src/components/Form/AutoCompleteWorkOrder.tsx delete mode 100644 src/components/Form/CurrencyInput.tsx delete mode 100644 src/components/Form/DatePicker.tsx delete mode 100644 src/components/Form/DateTimePicker.tsx delete mode 100644 src/components/Form/PhoneInput.tsx delete mode 100644 src/components/Form/Select.tsx delete mode 100644 src/components/Form/TextInput.tsx create mode 100644 src/components/Form/inputs/AutoComplete.tsx create mode 100644 src/components/Form/inputs/AutoCompleteContact.tsx create mode 100644 src/components/Form/inputs/AutoCompleteMyContacts.tsx create mode 100644 src/components/Form/inputs/AutoCompleteStates.tsx create mode 100644 src/components/Form/inputs/AutoCompleteWorkOrder.tsx create mode 100644 src/components/Form/inputs/AutoCompleteWorkOrderCategory.tsx create mode 100644 src/components/Form/inputs/BaseTextField.tsx create mode 100644 src/components/Form/inputs/CurrencyInput.tsx create mode 100644 src/components/Form/inputs/DatePicker.tsx create mode 100644 src/components/Form/inputs/DateTimePicker.tsx rename src/components/Form/{ => inputs}/PasswordInput.tsx (75%) create mode 100644 src/components/Form/inputs/PhoneInput.tsx create mode 100644 src/components/Form/inputs/RegionInput.tsx create mode 100644 src/components/Form/inputs/Select.tsx rename src/components/Form/{ => inputs}/Slider.tsx (69%) create mode 100644 src/components/Form/inputs/SliderWorkOrderPriority.tsx create mode 100644 src/components/Form/inputs/TextInput.tsx create mode 100644 src/components/Form/inputs/index.ts delete mode 100644 src/components/Form/types.ts delete mode 100644 src/components/Form/useDefaultTextFieldVariant.ts delete mode 100644 src/components/Form/useFormikFieldProps.ts diff --git a/src/components/Form/AutoComplete.tsx b/src/components/Form/AutoComplete.tsx deleted file mode 100644 index f30f794b..00000000 --- a/src/components/Form/AutoComplete.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import React, { useState, useEffect } from "react"; -import TextField, { type TextFieldProps } from "@mui/material/TextField"; -import { - StyledAutoComplete, - type StyledAutoCompleteProps, -} from "@components/Inputs/StyledAutoComplete"; -import { formClassNames } from "./classNames"; -import { useFormikFieldProps } from "./useFormikFieldProps"; -import type { AutocompleteValue } from "@mui/base/useAutocomplete"; -import type { AutocompleteRenderInputParams } from "@mui/material/Autocomplete"; - -/** - * A MUI Autocomplete with Formik integration which takes an optional type - * arg `OptionType` which defaults to `AutoCompleteOption` if not specified. - * Options must at least contain an `id` property, as well as a `label` - * property unless `getOptionLabel` is provided and the function utilizes - * other option properties. See `AutoCompleteProps` for more type info. - * - * This component is a controlled MUI Autocomplete, which necessitates the - * management of two separate state values: - * - * 1. `inputValue`: The value of the text input at any given time during focused - * input, which is managed here with internal state var `textFieldValue`. - * - * 2. `value`: The form field's selected value, which is managed here with internal - * state var `selectedOption` and Formik ctx hook `setFieldValue`. - */ -export const AutoComplete = < - OptionT extends AutoCompleteOption = AutoCompleteOption, - FreeSolo extends boolean = false ->({ - id, - label, - options, - isOptionEqualToValue, - doAfterSetSelectedOption, - // value-determining props: - freeSolo, - isValueNullable = true, - // behavior-determining props: - autoComplete = true, - autoHighlight = true, - autoSelect = false, - blurOnSelect = true, - clearOnEscape = true, - includeInputInList = true, - openOnFocus = true, - renderInput, - variant: explicitTextFieldVariant, - placeholder: explicitPlaceholder, - InputProps: explicitTextFieldInputProps = {}, - emptySelectionOption, - style, - ...props -}: AutoCompleteProps) => { - const [ - { value: fieldValue, error: fieldIsInvalid, helperText: fieldErrorMessage, variant }, - { setValue: setFormFieldValue }, - ] = useFormikFieldProps({ - id, - variant: explicitTextFieldVariant, - placeholder: explicitPlaceholder, - }); - - const [selectedOption, setSelectedOption] = useState>(null); - const [textFieldValue, setTextFieldValue] = useState(""); - - /* This useEffect ensures that if the form field's value is set externally, - the input is updated accordingly to reflect the new value. For example, during - create operations in the InvoiceForm, the selection of a WorkOrder will cause - the assignedTo input to reflect the WO's createdBy User. */ - useEffect(() => { - if (fieldValue !== ((selectedOption as any)?.id ?? selectedOption)) { - const newSelectedOptionValue = fieldValue - ? options.find((option) => fieldValue === option?.id ?? option) - : null; - setSelectedOption(newSelectedOptionValue ?? null); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fieldValue]); - - const handleChangeSelectedOption = ( - event: React.SyntheticEvent, - value: AutoCompleteValues - ) => { - setSelectedOption(value); - setFormFieldValue( - typeof value === "string" - ? value - : !!value && value?.id - ? value.id - : isValueNullable - ? null - : "" - ); - if (doAfterSetSelectedOption) doAfterSetSelectedOption(value); - }; - - const handleChangeTextInput = ( - event: React.SyntheticEvent, - newInputValue: string - ) => { - setTextFieldValue(newInputValue); - }; - - // Assign a default isOptionEqualToValue if one isn't provided - isOptionEqualToValue ??= (option, value) => (option?.id ?? option) === (value?.id ?? value); - - // Assign a default renderInput if one isn't provided - renderInput ??= ({ InputProps, ...params }) => ( - - ); - - return ( - - id={id} - options={options} - // state values and handlers: - value={selectedOption} - onChange={handleChangeSelectedOption} - inputValue={textFieldValue} - onInputChange={handleChangeTextInput} - isOptionEqualToValue={isOptionEqualToValue} - // autocomplete behavior props: - autoComplete={autoComplete} - autoHighlight={autoHighlight} - autoSelect={autoSelect} - blurOnSelect={blurOnSelect} - clearOnEscape={clearOnEscape} - disableClearable={false} - includeInputInList={includeInputInList} - multiple={false} - openOnFocus={openOnFocus} - // aesthetic props and the rest: - renderInput={renderInput} - noOptionsText={emptySelectionOption?.label ?? "--"} - style={style} - className={formClassNames.autoCompleteInput} - {...props} - /> - ); -}; - -/** - * A wrapper type around the `AutocompleteValue` generic exported by `@mui/base/useAutocomplete`. - * Type params `Multiple` and `DisableClearable` are set to false since they're not supported by - * this component. - */ -export type AutoCompleteValues< - OptionType extends AutoCompleteOption = AutoCompleteOption, - FreeSolo extends boolean = false -> = AutocompleteValue; - -/** - * The base type for AutoComplete props. An optional type arg `OptionType` can - * be provided to specify the type of option objects. If not provided, this type - * parameter defaults to `AutoCompleteOption`. - * - * **STATE HOOKS:** - * - Use `doAfterSetSelectedOption` to perform additional operations with the - * selected option. - * - * **USAGE NOTES:** - * - For grouped options, each option must have a `group` property string value. - * - Use the `emptySelectionOption` prop to provide a custom option for empty/null - * selections. - * - The following props, if provided, will be provided to the default `renderInput` - * TextField component: - * - `variant` - * - `InputProps` - */ -export type AutoCompleteProps< - OptionType extends AutoCompleteOption = AutoCompleteOption, - FreeSolo extends boolean = false -> = Omit< - StyledAutoCompleteProps, - "renderInput" // re-written below, converted to optional -> & { - id: string; - label?: React.ReactNode; - doAfterSetSelectedOption?: (option: AutoCompleteValues) => void; - isValueNullable?: boolean; - renderInput?: (params: AutocompleteRenderInputParams) => React.ReactNode; - variant?: TextFieldProps["variant"]; // given to default renderInput TextField - InputProps?: TextFieldProps["InputProps"]; // given to default renderInput TextField - groupBy?: (option: OptionType & { group: string }) => string; - emptySelectionOption?: OptionType | AutoCompleteOption; -}; - -export interface AutoCompleteOption { - id: string; - label?: string; - group?: string; - [K: string]: any; -} - -export type AutoCompleteOptions = Array; diff --git a/src/components/Form/AutoCompleteContact.tsx b/src/components/Form/AutoCompleteContact.tsx deleted file mode 100644 index 9ba6ac8a..00000000 --- a/src/components/Form/AutoCompleteContact.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useQuery } from "@apollo/client/react/hooks"; -import Box from "@mui/material/Box"; -import Text from "@mui/material/Typography"; -import { Avatar } from "@components/Avatar"; -import { QUERIES } from "@graphql/queries"; -import { AutoComplete, type AutoCompleteProps } from "./AutoComplete"; -import type { Profile, Contact } from "@graphql/types"; - -/** - * AutoCompleteContact - * - Uses `MyContacts` GQL query with "cache-only" fetch policy - * - Uses MUI Autocomplete input - */ -export const AutoCompleteContact = ({ - reduceContacts = (contacts) => contacts as Contact[], - emptySelectionOption, - ...props -}: AutoCompleteContactProps) => { - const { data, loading } = useQuery(QUERIES.MY_CONTACTS, { - fetchPolicy: "cache-only", // For this input, only pull from cache - }); - - const contactOptions: AutoCompleteContactOptions = []; - - if ( - !loading && - data?.myContacts && - Array.isArray(data.myContacts) && - data.myContacts.length > 0 - ) { - contactOptions.concat(reduceContacts(data.myContacts)); - } - - const renderOptionFallback = - emptySelectionOption && "label" in emptySelectionOption - ? `- ${emptySelectionOption.label} -` - : "--"; - - const handleRenderOption = ( - props: React.HTMLAttributes, - { profile }: AutoCompleteContactOption - ) => ( - - ); - - const handleGetOptionLabel = ({ profile, handle }: AutoCompleteContactOption) => - `${profile?.displayName || handle}`; - - return ( - - ); -}; - -const AutoCompleteContactOptionListItem = ({ - profile, - renderFallback = "--", - ...props -}: { - profile?: Profile; - renderFallback?: React.ReactNode; -} & React.HTMLAttributes) => ( - - {profile ? ( - - ) : ( - {renderFallback} - )} - -); - -export type AutoCompleteContactProps = { - reduceContacts?: (contacts: Array) => AutoCompleteContactOptions; -} & Omit< - AutoCompleteProps, - "options" | "renderOption" | "getOptionLabel" ->; - -export type AutoCompleteContactOption = Contact; -export type AutoCompleteContactOptions = Array; diff --git a/src/components/Form/AutoCompleteStates.tsx b/src/components/Form/AutoCompleteStates.tsx deleted file mode 100644 index a49234c3..00000000 --- a/src/components/Form/AutoCompleteStates.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { AutoComplete, type AutoCompleteProps, type AutoCompleteOption } from "./AutoComplete"; - -export const AutoCompleteStates = (props: AutoCompleteStatesProps) => ( - option.group} - autoSelect - {...props} - /> -); - -export type AutoCompleteStatesProps = Omit< - AutoCompleteProps, - "options" | "groupBy" | "autoSelect" ->; - -const US_STATES_AND_TERRITORIES = [ - "Alabama", - "Alaska", - "Arizona", - "Arkansas", - "California", - "Colorado", - "Connecticut", - "Delaware", - "District of Columbia", - "Florida", - "Georgia", - "Hawaii", - "Idaho", - "Illinois", - "Indiana", - "Iowa", - "Kansas", - "Kentucky", - "Louisiana", - "Maine", - "Maryland", - "Massachusetts", - "Michigan", - "Minnesota", - "Mississippi", - "Missouri", - "Montana", - "Nebraska", - "Nevada", - "New Hampshire", - "New Jersey", - "New Mexico", - "New York", - "North Carolina", - "North Dakota", - "Ohio", - "Oklahoma", - "Oregon", - "Pennsylvania", - "Rhode Island", - "South Carolina", - "South Dakota", - "Tennessee", - "Texas", - "Utah", - "Vermont", - "Virginia", - "Washington", - "West Virginia", - "Wisconsin", - "Wyoming", -] - .map((placeName) => ({ id: placeName, label: placeName, group: "States" })) - .concat( - ["American Samoa", "Guam", "Northern Mariana Islands", "Puerto Rico", "US Virgin Islands"].map( - (placeName) => ({ id: placeName, label: placeName, group: "Territories" }) - ) - ); diff --git a/src/components/Form/AutoCompleteWorkOrder.tsx b/src/components/Form/AutoCompleteWorkOrder.tsx deleted file mode 100644 index c5895584..00000000 --- a/src/components/Form/AutoCompleteWorkOrder.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import Box from "@mui/material/Box"; -import Text from "@mui/material/Typography"; -import { Avatar } from "@components/Avatar"; -import { getDateAndTime } from "@utils/dateTime"; -import { AutoComplete, type AutoCompleteProps } from "./AutoComplete"; -import type { WorkOrder, FixitUser } from "@graphql/types"; - -/** - * AutoCompleteWorkOrder is a Mui Autocomplete input used to select a WorkOrder - * from the provided list of WorkOrder `options`. - * - * @note This component uses a `multiline` Input, which with Mui's Autocomplete - * currently causes the following console warning in non-prod envs: `A textarea - * element was provided to Autocomplete where input was expected.`. This warning - * currently only appears in dev, so is being ignored for now. - */ -export const AutoCompleteWorkOrder = ({ - InputProps = {}, - ...props -}: AutoCompleteWorkOrderProps) => { - const handleRenderOption = ( - props: React.HTMLAttributes, - { _renderUser, location, createdAt }: AutoCompleteWorkOrderOption - ) => ( - - ); - - const handleGetOptionLabel = ({ - _renderUser: { profile, handle }, - location: { streetLine1 }, - createdAt, - }: AutoCompleteWorkOrderOption) => - `${profile?.displayName || handle}\n${streetLine1}\n${getDateAndTime(createdAt)}`; - - return ( - - ); -}; - -const SelectableWorkOrderOption = ({ - _renderUser, // createdBy or assignedTo - location, - createdAt, - ...props -}: { _renderUser: FixitUser } & Pick & - React.HTMLAttributes) => ( - *": { - maxWidth: "100%", - }, - }} - {...props} - > - - {location.streetLine1} - {getDateAndTime(createdAt)} - -); - -export type AutoCompleteWorkOrderProps = Omit< - AutoCompleteProps, - "renderOption" | "getOptionLabel" ->; - -/** - * AutoCompleteWorkOrderOption is an AutoCompleteOption with additional internal - * property `_renderUser`, which objects must include to indicate the user that - * will be rendered in the option and label. - */ -export type AutoCompleteWorkOrderOption = WorkOrder & { _renderUser: FixitUser }; -export type AutoCompleteWorkOrderOptions = Array; diff --git a/src/components/Form/CurrencyInput.tsx b/src/components/Form/CurrencyInput.tsx deleted file mode 100644 index 74b1ab2f..00000000 --- a/src/components/Form/CurrencyInput.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import InputAdornment from "@mui/material/InputAdornment"; -import Text from "@mui/material/Typography"; -import { StyledTextField } from "@components/Inputs/StyledTextField"; -import { - NumericFormatTextInput, - type NumericFormatTextInputProps, -} from "@components/Inputs/TextInputNumericFormat"; -import { formClassNames } from "./classNames"; -import { useFormikFieldProps } from "./useFormikFieldProps"; - -/** - * TextInput which uses `react-number-format` for currency formatting. - * - * **NOTE:** Setting prop `type="number"` is not recommended, since the resultant - * HTML element is prone to being unintentionally changed via mouse scroll wheel. - * See https://mui.com/material-ui/react-text-field/#type-quot-number-quot - */ -export const CurrencyInput = ({ - id, - placeholder: explicitPlaceholder = "0.00", - variant: explicitVariant, - InputProps = {}, - ...props -}: CurrencyInputProps) => { - const [{ value: fieldValue, ...textInputProps }] = useFormikFieldProps({ - id, - placeholder: explicitPlaceholder, - variant: explicitVariant, - }); - - return ( - - $ - - ), - sx: { - "& input": { textAlign: "right" }, - ...(InputProps?.sx ?? {}), - }, - }} - {...textInputProps} - {...props} - // NumericFormat props: - customInput={StyledTextField} - allowLeadingZeros={false} - allowNegative={false} - decimalScale={2} - fixedDecimalScale - thousandSeparator - valueIsNumericString - /> - ); -}; - -export type CurrencyInputProps = Omit< - NumericFormatTextInputProps, - | "type" - | "autoComplete" - | "customInput" - | "allowLeadingZeros" - | "allowNegative" - | "decimalScale" - | "fixedDecimalScale" - | "thousandSeparator" - | "valueIsNumericString" -> & { - id: string; - autoComplete?: "transaction-amount"; - InputProps?: Omit; -}; diff --git a/src/components/Form/DatePicker.tsx b/src/components/Form/DatePicker.tsx deleted file mode 100644 index e19bc5e2..00000000 --- a/src/components/Form/DatePicker.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { StyledDatePicker, type StyledDatePickerProps } from "@components/Inputs/StyledDatePicker"; -import { formClassNames } from "./classNames"; -import { useFormikFieldProps } from "./useFormikFieldProps"; -import type { TextFieldProps } from "@mui/material/TextField"; - -/** - * Mui DatePicker with Formik bindings and app-specific additions: - * - Mui-system grid props like `gridArea` - * - `variant` and `style` props, if provided, are passed to the `TextField` slot - * (defaults to `"outlined"` on mobile and `"filled"` on desktop). - * - * Usage example: - * ``` - * - * ``` - */ -export const DatePicker = ({ - id, - variant: explicitVariant, - style, - slotProps = {}, - ...props -}: DatePickerProps) => { - const [{ value: fieldValue, onChange: handleFieldValueChange, variant }] = - useFormikFieldProps({ - id, - variant: explicitVariant, - }); - - const handleChange = (value: TDate | null) => { - handleFieldValueChange(value); - }; - - return ( - - value={fieldValue} - onChange={handleChange} - className={formClassNames.dateInput} - slotProps={{ - textField: { - variant, - style, - ...(slotProps?.textField ?? {}), - }, - ...slotProps, - }} - {...props} - /> - ); -}; - -export type DatePickerProps = { - id: string; - variant?: TextFieldProps["variant"]; - style?: React.CSSProperties; -} & Omit, "value" | "onChange">; diff --git a/src/components/Form/DateTimePicker.tsx b/src/components/Form/DateTimePicker.tsx deleted file mode 100644 index 2f83806a..00000000 --- a/src/components/Form/DateTimePicker.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { - StyledDateTimePicker, - type StyledDateTimePickerProps, -} from "@components/Inputs/StyledDateTimePicker"; -import { formClassNames } from "./classNames"; -import { useFormikFieldProps } from "./useFormikFieldProps"; -import type { TextFieldProps } from "@mui/material/TextField"; - -export const DateTimePicker = ({ - id, - variant: explicitVariant, - style, - slotProps = {}, - ...props -}: DateTimePickerProps) => { - const [{ value: fieldValue, onChange: handleFieldValueChange, variant }] = - useFormikFieldProps({ - id, - variant: explicitVariant, - }); - - const handleChange = (value: TDate | null) => { - handleFieldValueChange(value); - }; - - return ( - - value={fieldValue} - onChange={handleChange} - className={formClassNames.dateTimeInput} - slotProps={{ - textField: { - variant, - style, - ...(slotProps?.textField ?? {}), - }, - ...slotProps, - }} - {...props} - /> - ); -}; - -export type DateTimePickerProps = { - id: string; - variant?: TextFieldProps["variant"]; - style?: React.CSSProperties; -} & Omit, "value" | "onChange">; diff --git a/src/components/Form/PhoneInput.tsx b/src/components/Form/PhoneInput.tsx deleted file mode 100644 index 7a34a90b..00000000 --- a/src/components/Form/PhoneInput.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { StyledTextField } from "@components/Inputs/StyledTextField"; -import { - PatternFormatTextInput, - type PatternFormatTextInputProps, -} from "@components/Inputs/TextInputPatternFormat"; -import { formClassNames } from "./classNames"; -import { useFormikFieldProps } from "./useFormikFieldProps"; - -/** - * TextInput which uses `react-number-format` for phone formatting. - * - * > Currently only the US phone format is supported. - */ -export const PhoneInput = ({ - id, - placeholder: explicitPlaceholder = "(123) 456-7890", - variant: explicitVariant, - ...props -}: PhoneInputProps) => { - const [{ value: fieldValue, ...textInputProps }] = useFormikFieldProps({ - id, - placeholder: explicitPlaceholder, - variant: explicitVariant, - }); - - return ( - - ); -}; - -export type PhoneInputProps = Omit< - PatternFormatTextInputProps, - "type" | "autoComplete" | "customInput" | "format" | "valueIsNumericString" -> & { - id: string; -}; diff --git a/src/components/Form/Select.tsx b/src/components/Form/Select.tsx deleted file mode 100644 index a907b30d..00000000 --- a/src/components/Form/Select.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useField, useFormikContext } from "formik"; -import FormControl from "@mui/material/FormControl"; -import InputLabel from "@mui/material/InputLabel"; -import MenuItem from "@mui/material/MenuItem"; -import MuiSelect from "@mui/material/Select"; -import { useDefaultTextFieldVariant } from "./useDefaultTextFieldVariant"; -import type { SelectProps as MuiSelectProps, SelectChangeEvent } from "@mui/material/Select"; - -export const Select = ({ - id, - label, - options, - variant: explicitVariant, - fullWidth = false, - styles = {}, - ...props -}: SelectProps) => { - const [field, meta] = useField(id); - const { setFieldValue, handleBlur } = useFormikContext(); - const defaultVariant = useDefaultTextFieldVariant(); - - const handleChangeSelect = (event: SelectChangeEvent) => { - setFieldValue(id, event.target.value as string); - }; - - const selectLabelID = `Select:InputLabel:${id}`; - - const muiVariant = explicitVariant ?? defaultVariant; - - return ( - - - {label ?? id} - - - {options.map(({ value, label }) => ( - - {label ?? value} - - ))} - - - ); -}; - -export type SelectProps = { - id: string; - label?: React.ReactNode; - options: SelectOptions; - styles?: { - container?: React.CSSProperties; - label?: React.CSSProperties; - select?: React.CSSProperties; - }; -} & Omit; - -export type SelectOptions = Array<{ value: string | number | null; label?: string }>; diff --git a/src/components/Form/TextInput.tsx b/src/components/Form/TextInput.tsx deleted file mode 100644 index 2c34bd5e..00000000 --- a/src/components/Form/TextInput.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { StyledTextField, type StyledTextFieldProps } from "@components/Inputs/StyledTextField"; -import { formClassNames } from "./classNames"; -import { useFormikFieldProps } from "./useFormikFieldProps"; -import type { AutoCompleteAttributeValue } from "./types"; - -export const TextInput = ({ - id, - label: explicitLabel, - placeholder: explicitPlaceholder, - variant: explicitVariant, - ...props -}: TextInputProps) => { - const [{ value, ...textInputProps }] = useFormikFieldProps({ - id, - label: explicitLabel, - placeholder: explicitPlaceholder, - variant: explicitVariant, - }); - - return ( - - ); -}; - -/** - * Props for TextInput and related/derived components. - */ -export type TextInputProps = Omit< - StyledTextFieldProps, - "label" | "autoComplete" | "value" | "onChange" | "onBlur" -> & { - id: string; - label?: string; - autoComplete?: AutoCompleteAttributeValue; -}; diff --git a/src/components/Form/inputs/AutoComplete.tsx b/src/components/Form/inputs/AutoComplete.tsx new file mode 100644 index 00000000..f453aa7d --- /dev/null +++ b/src/components/Form/inputs/AutoComplete.tsx @@ -0,0 +1,284 @@ +import { useState, useEffect } from "react"; +import { grid as muiGridSxProps, type GridProps as MuiGridSxProps } from "@mui/system"; +import { styled } from "@mui/material/styles"; +import MuiAutocomplete, { + type AutocompleteProps as MuiAutocompleteProps, +} from "@mui/material/Autocomplete"; +import TextField, { type TextFieldProps } from "@mui/material/TextField"; +import { getTypeSafeError } from "@/utils/typeSafety/getTypeSafeError"; +import { formClassNames } from "../classNames"; +import { useFormikFieldProps, type FormikIntegratedInputProps } from "../helpers"; +import type { AutocompleteValue } from "@mui/base/useAutocomplete"; +import type { Simplify, SetRequired, SetOptional } from "type-fest"; + +/** + * A MUI Autocomplete with Formik integration which takes an optional type arg `OptionType` which + * defaults to {@link BaseAutoCompleteOption|`BaseAutoCompleteOption`} if not specified. Options + * must at least contain an `id` field, as well as a `label` unless `getOptionLabel` is provided + * and the function utilizes other option properties. See {@link AutoCompleteProps} for more type + * info. + * + * This component is a controlled MUI Autocomplete, which necessitates the management of two + * separate state values: + * + * 1. `inputValue`: The value of the text input at any given time during focused + * input, which is managed here with internal state var `textFieldValue`. + * + * 2. `value`: The form field's selected value, which is managed here with internal + * state var `selectedOption` and Formik ctx hook `setFieldValue`. + */ +export const AutoComplete = < + OptionType extends BaseAutoCompleteOption = BaseAutoCompleteOption, + Multiple extends boolean = false, + DisableClearable extends boolean = false, + FreeSolo extends boolean = false, + ChipComponent extends React.ElementType = "div", +>({ + id, + label, + options, + getFieldValueFromOption: caller_getFieldValueFromOption, + getOptionFromFieldValue: caller_getOptionFromFieldValue, + onChange: caller_onChange, + onInputChange: caller_onInputChange, + // behavior-determining props: + autoComplete = true, + autoHighlight = true, + autoSelect = false, + blurOnSelect = true, + clearOnEscape = true, + includeInputInList = true, + openOnFocus = true, + // aesthetic props and the rest: + renderInput, + variant: explicitTextFieldVariant, + placeholder: explicitPlaceholder, + InputProps: explicitTextFieldInputProps = {}, + style, + ...autoCompleteProps +}: AutoCompleteProps) => { + // Short-hand for the type of the Autocomplete's `value` prop: + type ValuePropType = AutocompleteValue; + + const [selectedOption, setSelectedOption] = useState(null); + const [textFieldValue, setTextFieldValue] = useState(""); + + const [ + { value: fieldValue, error: fieldIsInvalid, helperText: fieldErrorMessage, variant }, + { setValue: setFormFieldValue, setError: setFormFieldErrorMessage }, + ] = useFormikFieldProps({ + fieldID: id, + variant: explicitTextFieldVariant, + placeholder: explicitPlaceholder, + }); + + // Defaults for getFieldValueFromOption and getOptionFromFieldValue: + + const getFieldValueFromOption = caller_getFieldValueFromOption + ? caller_getFieldValueFromOption + : (opt: OptionType | ValuePropType | null) => { + const targetFieldValue = !!opt && "id" in opt ? opt.id : opt; + return targetFieldValue as string | null; + }; + + const getOptionFromFieldValue = caller_getOptionFromFieldValue + ? caller_getOptionFromFieldValue + : (fieldValueArg: string | null) => { + const targetOption = fieldValueArg + ? options.find((opt) => opt.id === fieldValueArg) ?? fieldValueArg + : fieldValueArg; + return targetOption as ValuePropType; + }; + + /* This useEffect ensures that if the form field's value is set externally, the input is updated + accordingly to reflect the new value. For example, during create operations in InvoiceForm, the + selection of a WorkOrder will cause the assignedTo input to reflect the WO's createdBy User. */ + useEffect(() => { + if (fieldValue !== getFieldValueFromOption(selectedOption)) { + const newFieldValue = getOptionFromFieldValue(fieldValue); + // Ensure newFieldValue !undefined, which would cause Mui to think the comp is uncontrolled. + if (newFieldValue !== undefined) setSelectedOption(newFieldValue); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fieldValue]); + + const handleChangeSelectedOption: AutoCompleteOnChangeFn< + OptionType, + Multiple, + DisableClearable, + FreeSolo + > = async (event, option, reason, details) => { + setSelectedOption(option); + try { + await setFormFieldValue(getFieldValueFromOption(option)); + if (caller_onChange) await caller_onChange(event, option, reason, details); + } catch (error) { + setFormFieldErrorMessage(getTypeSafeError(error).message); + } + }; + + const handleChangeTextInput: AutoCompleteOnInputChangeFn = async ( + event, + newInputValue, + reason + ) => { + setTextFieldValue(newInputValue); + if (caller_onInputChange) await caller_onInputChange(event, newInputValue, reason); + }; + + // Assign a default renderInput if one isn't provided + renderInput ??= ({ InputProps, ...params }) => ( + + ); + + // FOR THE `value` PROP, `||` is used over `??` to ensure empty strings are not provided as `value` prop. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const autoCompleteValue = selectedOption || (null as ValuePropType); + + return ( + + id={id} + options={options} + // state values and handlers: + value={autoCompleteValue} + onChange={handleChangeSelectedOption} + inputValue={textFieldValue} + onInputChange={handleChangeTextInput} + // autocomplete behavior props: + autoComplete={autoComplete} + autoHighlight={autoHighlight} + autoSelect={autoSelect} + blurOnSelect={blurOnSelect} + clearOnEscape={clearOnEscape} + includeInputInList={includeInputInList} + openOnFocus={openOnFocus} + // aesthetic props and the rest: + renderInput={renderInput} + className={formClassNames.autoCompleteInput} + style={style} + {...autoCompleteProps} + /> + ); +}; + +const StyledAutoComplete = styled(MuiAutocomplete, { + shouldForwardProp: (propName: string) => !propName.startsWith("grid"), +})(muiGridSxProps) as typeof MuiAutocomplete; + +/** + * The base type for AutoComplete props. An optional type arg `OptionType` can be provided + * to specify the type of option objects. If not provided, this type parameter defaults to + * {@link BaseAutoCompleteOption|`BaseAutoCompleteOption`}. + * + * ### State Hooks: + * - Use `doAfterSetSelectedOption` to perform additional operations with the selected option. + * + * ### Usage Notes: + * - For grouped options, each option must have a `group` property string value. + * - Use the `emptySelectionOption` prop to provide a custom option for empty/null selections. + * - The following props, if provided, will be provided to the default `renderInput` TextField comp: + * - `variant` + * - `InputProps` + */ +export type AutoCompleteProps< + BaseOptionType extends BaseAutoCompleteOption = BaseAutoCompleteOption, + Multiple extends boolean | undefined = false, + DisableClearable extends boolean | undefined = false, + FreeSolo extends boolean | undefined = false, + ChipComponent extends React.ElementType = "div", +> = Simplify< + SetOptional< + FormikIntegratedInputProps< + MuiAutocompleteProps, + "onChange" | "onInputChange" + >, + "renderInput" + > & { + groupBy?: (option: SetRequired) => string; + // Custom functions: + getFieldValueFromOption?: AutoCompleteGetFieldValueFromOptionFn; // prettier-ignore + getOptionFromFieldValue?: AutoCompleteGetOptionFromFieldValueFn; // prettier-ignore + } & Pick & // <-- props passed to default renderInput TextField + MuiGridSxProps +>; + +/////////////////////////////////////////////////////////////////////////////// +// AutoComplete utility types: + +/** + * The base/default type used for the first type parameter of {@link AutoCompleteProps}. + */ +export type BaseAutoCompleteOption = { + id: string; + label?: string; + group?: string; + [K: string]: unknown; +}; + +/** + * An array of {@link BaseAutoCompleteOption} objects. + */ +export type BaseAutoCompleteOptions = Array; + +/////////////////////////////////////////////////////////////////////////////// +// AutoComplete function types: + +/** + * `getFieldValueFromOption` function type for {@link AutoComplete}. + */ +export type AutoCompleteGetFieldValueFromOptionFn< + OptionType extends BaseAutoCompleteOption = BaseAutoCompleteOption, + Multiple extends boolean | undefined = false, + DisableClearable extends boolean | undefined = false, + FreeSolo extends boolean | undefined = false, +> = ( + option: OptionType | AutocompleteValue | null +) => string | null; + +/** + * `getOptionFromFieldValue` function type for {@link AutoComplete}. + */ +export type AutoCompleteGetOptionFromFieldValueFn< + OptionType extends BaseAutoCompleteOption = BaseAutoCompleteOption, + Multiple extends boolean | undefined = false, + DisableClearable extends boolean | undefined = false, + FreeSolo extends boolean | undefined = false, +> = ( + fieldValue: string | null +) => AutocompleteValue; + +/** + * `renderInput` function type for {@link AutoComplete}. + * @note The `renderInput` function's type does not use any of the AutoComplete type params. + */ +export type AutoCompleteRenderInputFn = NonNullable< + MuiAutocompleteProps["renderInput"] +>; + +/** + * `onChange` function type for {@link AutoComplete}. + */ +export type AutoCompleteOnChangeFn< + OptionType extends BaseAutoCompleteOption = BaseAutoCompleteOption, + Multiple extends boolean | undefined = false, + DisableClearable extends boolean | undefined = false, + FreeSolo extends boolean | undefined = false, +> = NonNullable["onChange"]>; + +/** + * `onInputChange` function type for {@link AutoComplete}. + * @note The `onInputChange` function's type does not use any of the AutoComplete type params. + */ +export type AutoCompleteOnInputChangeFn = NonNullable< + MuiAutocompleteProps["onInputChange"] +>; diff --git a/src/components/Form/inputs/AutoCompleteContact.tsx b/src/components/Form/inputs/AutoCompleteContact.tsx new file mode 100644 index 00000000..606f41a4 --- /dev/null +++ b/src/components/Form/inputs/AutoCompleteContact.tsx @@ -0,0 +1,47 @@ +import { ContactListItem } from "@/components/List/listItems/ContactListItem"; +import { AutoComplete, type AutoCompleteProps } from "./AutoComplete"; +import type { Contact } from "@/graphql/types"; + +/** + * `AutoCompleteContact` is a Mui Autocomplete input used to select a Contact + * from the provided list of Contact `options`. + */ +export const AutoCompleteContact = ({ + renderOption, + getOptionLabel, + ...autoCompleteProps +}: AutoCompleteContactProps) => { + // Assign default fns: + renderOption ??= defaultRenderOption; + getOptionLabel ??= defaultGetOptionLabel; + + return ( + + ); +}; + +/** + * Default `renderOption` fn for {@link AutoCompleteContact}. + */ +const defaultRenderOption: NonNullable< + AutoCompleteProps["renderOption"] +> = ( + listItemProps, + contact // option + // other available props: state, ownerState +) => ; + +/** + * Default `getOptionLabel` fn for {@link AutoCompleteContact}. + */ +const defaultGetOptionLabel: NonNullable< + AutoCompleteProps["getOptionLabel"] +> = ({ handle }) => handle; + +export type AutoCompleteContactProps = AutoCompleteProps; +export type AutoCompleteContactOption = Contact; +export type AutoCompleteContactOptions = Array; diff --git a/src/components/Form/inputs/AutoCompleteMyContacts.tsx b/src/components/Form/inputs/AutoCompleteMyContacts.tsx new file mode 100644 index 00000000..18944e9b --- /dev/null +++ b/src/components/Form/inputs/AutoCompleteMyContacts.tsx @@ -0,0 +1,41 @@ +import { useMemo } from "react"; +import { useQuery } from "@apollo/client/react/hooks"; +import { QUERIES } from "@/graphql/queries"; +import { + AutoCompleteContact, + type AutoCompleteContactProps, + type AutoCompleteContactOptions, +} from "./AutoCompleteContact"; +import type { Simplify } from "type-fest"; + +/** + * `AutoCompleteMyContacts` displays the results of the `MyContacts` GQL query + * (with `fetchPolicy: "cache-only"`), in an `AutoCompleteContact` component. + */ +export const AutoCompleteMyContacts = ({ + reduceContacts = defaultReduceContacts, + ...autoCompleteContactProps +}: AutoCompleteMyContactsProps) => { + const { data, loading } = useQuery(QUERIES.MY_CONTACTS, { + fetchPolicy: "cache-only", // For this input, only pull from cache + }); + + const contactOptions: AutoCompleteContactOptions = useMemo(() => { + return !loading && Array.isArray(data?.myContacts) && data.myContacts.length > 0 + ? reduceContacts(data.myContacts) + : []; + }, [data, loading, reduceContacts]); + + return ; +}; + +/** + * Default `reduceContacts` fn for {@link AutoCompleteContact}. + */ +const defaultReduceContacts = (contacts: AutoCompleteContactOptions) => contacts; + +export type AutoCompleteMyContactsProps = Simplify< + { + reduceContacts?: (contacts: AutoCompleteContactOptions) => AutoCompleteContactOptions; + } & Omit +>; diff --git a/src/components/Form/inputs/AutoCompleteStates.tsx b/src/components/Form/inputs/AutoCompleteStates.tsx new file mode 100644 index 00000000..fe4b593b --- /dev/null +++ b/src/components/Form/inputs/AutoCompleteStates.tsx @@ -0,0 +1,55 @@ +import { AutoComplete, type AutoCompleteProps, type BaseAutoCompleteOption } from "./AutoComplete"; +import type { OverrideProperties } from "type-fest"; + +export const AutoCompleteStates = ({ ...autoCompleteProps }: AutoCompleteStatesProps) => ( + +); + +/** + * Default `groupBy` fn for {@link AutoCompleteStates}. + */ +const defaultGroupBy = (option: AutoCompleteStatesOption) => option.group; + +export type AutoCompleteStatesProps = Omit< + AutoCompleteProps, + "options" | "groupBy" | "autoSelect" +>; + +export type AutoCompleteStatesOption = OverrideProperties< + Required, + { group: "States" | "Territories" } +>; + +// prettier-ignore +const US_STATES = [ + "Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", + "District of Columbia", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", + "Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", + "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", + "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", + "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", + "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming" +] as const; + +// prettier-ignore +const US_TERRITORIES = [ + "American Samoa", "Guam", "Northern Mariana Islands", "Puerto Rico", "US Virgin Islands", +] as const; + +const US_STATES_AND_TERRITORIES_OPTIONS: Array = [ + ...US_STATES.map((stateName) => ({ + id: stateName, + label: stateName, + group: "States", + })), + ...US_TERRITORIES.map((territoryName) => ({ + id: territoryName, + label: territoryName, + group: "Territories", + })), +]; diff --git a/src/components/Form/inputs/AutoCompleteWorkOrder.tsx b/src/components/Form/inputs/AutoCompleteWorkOrder.tsx new file mode 100644 index 00000000..3652dd56 --- /dev/null +++ b/src/components/Form/inputs/AutoCompleteWorkOrder.tsx @@ -0,0 +1,76 @@ +import dayjs from "dayjs"; +import { AutoComplete, type AutoCompleteProps } from "@/components/Form/inputs"; +import { + WorkOrderListItem, + type WorkOrderListItemProps, +} from "@/components/List/listItems/WorkOrderListItem"; + +/** + * `AutoCompleteWorkOrder` is a Mui Autocomplete input used to select a WorkOrder + * from the provided list of WorkOrder `options`. + * + * @note This component uses a `multiline` Input, which with Mui's Autocomplete + * currently causes the following console warning in non-prod envs: `A textarea + * element was provided to Autocomplete where input was expected.`. This warning + * currently only appears in dev, so is being ignored for now. + */ +export const AutoCompleteWorkOrder = ({ + renderOption, + getOptionLabel, + InputProps = {}, + ...autoCompleteProps +}: AutoCompleteWorkOrderProps) => { + // Assign default fns: + renderOption ??= defaultRenderOption; + getOptionLabel ??= defaultGetOptionLabel; + + return ( + + ); +}; + +/** + * Default `renderOption` fn for {@link AutoCompleteWorkOrder}. + */ +const defaultRenderOption: NonNullable = ( + listItemProps, + { userToDisplay, ...workOrder } // option + // other available props: state, ownerState +) => ; + +/** + * Default `getOptionLabel` fn for {@link AutoCompleteWorkOrder}. + */ +const defaultGetOptionLabel: NonNullable = ({ + userToDisplay, + location: { streetLine1 }, + createdAt, +}) => { + const userDescription = userToDisplay?.profile?.displayName ?? "- Unassigned -"; + return `${userDescription}\n${streetLine1}\n${dayjs(createdAt).format("M/D/YY h:mm a")}`; +}; + +export type AutoCompleteWorkOrderProps = AutoCompleteProps; + +/** + * AutoCompleteWorkOrderOption is a `WorkOrder` with additional internal property + * `userToDisplay`, which the {@link WorkOrderListItem|`WorkOrderListItem`} component + * in the {@link defaultRenderOption|`defaultRenderOption` fn} uses to determine which + * of the WorkOrder's associated users to display (i.e. `createdBy` or `assignedTo`). + */ +export type AutoCompleteWorkOrderOption = WorkOrderListItemProps["workOrder"] & + Pick; + +export type AutoCompleteWorkOrderOptions = Array; diff --git a/src/components/Form/inputs/AutoCompleteWorkOrderCategory.tsx b/src/components/Form/inputs/AutoCompleteWorkOrderCategory.tsx new file mode 100644 index 00000000..a738a065 --- /dev/null +++ b/src/components/Form/inputs/AutoCompleteWorkOrderCategory.tsx @@ -0,0 +1,51 @@ +import { WO_CATEGORY_ICONS_JSX } from "@/components/Icons/WorkOrderCategoryIcon"; +import { WorkOrderCategoryListItem } from "@/components/List/listItems/WorkOrderCategoryListItem"; +import { WORK_ORDER_CATEGORIES } from "@/types/WorkOrder"; +import { AutoComplete, type AutoCompleteProps, type BaseAutoCompleteOption } from "./AutoComplete"; +import type { WorkOrderCategory } from "@/graphql/types"; +import type { OverrideProperties } from "type-fest"; + +/** + * AutoCompleteWorkOrderCategory + */ +export const AutoCompleteWorkOrderCategory = ({ + renderOption, + ...autoCompleteProps +}: AutoCompleteWorkOrderCategoryProps) => { + // Assign default renderOption fn: + renderOption ??= defaultRenderOption; + + return ( + + ); +}; + +const defaultRenderOption: NonNullable = ( + props, + { label, icon } +) => ; + +const WORK_ORDER_CATEGORY_OPTIONS: Array = + WORK_ORDER_CATEGORIES.map((category) => ({ + id: category, + label: category, + icon: WO_CATEGORY_ICONS_JSX[category], + })); + +export type AutoCompleteWorkOrderCategoryProps = Omit< + AutoCompleteProps, + "options" +>; + +export type AutoCompleteWorkOrderCategoryOption = OverrideProperties< + BaseAutoCompleteOption, + { + id: WorkOrderCategory; + label: WorkOrderCategory; + icon: React.ReactNode; + } +>; diff --git a/src/components/Form/inputs/BaseTextField.tsx b/src/components/Form/inputs/BaseTextField.tsx new file mode 100644 index 00000000..f578e249 --- /dev/null +++ b/src/components/Form/inputs/BaseTextField.tsx @@ -0,0 +1,34 @@ +import { grid as muiGridSxProps, type GridProps as MuiGridSxProps } from "@mui/system"; +import { styled } from "@mui/material/styles"; +import { formHelperTextClasses } from "@mui/material/FormHelperText"; +import { formLabelClasses } from "@mui/material/FormLabel"; +import MuiTextField, { + type TextFieldProps as MuiTextFieldProps, + type TextFieldVariants as MuiTextFieldVariants, +} from "@mui/material/TextField"; + +/** + * This component serves as the base `TextField` component for the app. + * + * It does not contain any implementation logic - it is simply a Mui TextField + * with minor custom styling and Mui-system grid props like `gridArea`. + * + * @example + * ``` + * + * ``` + */ +export const BaseTextField = styled(MuiTextField, { + shouldForwardProp: (propName: string) => !propName.startsWith("grid"), +})({ + [`& .${formLabelClasses.root}`]: { + textTransform: "capitalize", + }, + [`& .${formHelperTextClasses.root}`]: { + whiteSpace: "nowrap", + }, + ...muiGridSxProps, +}); + +export type BaseTextFieldProps = + MuiTextFieldProps & MuiGridSxProps; diff --git a/src/components/Form/inputs/CurrencyInput.tsx b/src/components/Form/inputs/CurrencyInput.tsx new file mode 100644 index 00000000..897a6a03 --- /dev/null +++ b/src/components/Form/inputs/CurrencyInput.tsx @@ -0,0 +1,83 @@ +import { NumericFormat, type NumericFormatProps } from "react-number-format"; +import InputAdornment from "@mui/material/InputAdornment"; +import Text from "@mui/material/Typography"; +import { BaseTextField, type BaseTextFieldProps } from "./BaseTextField"; +import { formClassNames } from "../classNames"; +import { useFormikFieldProps, type FormikIntegratedInputProps } from "../helpers"; +import type { Simplify } from "type-fest"; + +/** + * TextInput which uses [react-number-format][rnf-docs] for currency formatting. + * + * #### Usage Notes + * - [Mui's TextField docs strongly recommend using `type="numeric"` over `type="number"` for + * numeric inputs][mui-type-prop], since the latter results in an HTML element that's prone + * to being unintentionally changed via mouse scroll-wheel. + * + * [rnf-docs]: https://s-yadav.github.io/react-number-format/docs/numeric_format + * [mui-type-prop]: https://mui.com/material-ui/react-text-field/#type-quot-number-quot + */ +export const CurrencyInput = ({ + id, + placeholder: explicitPlaceholder = "0.00", + variant: explicitVariant, + InputProps = {}, + ...props +}: CurrencyInputProps) => { + const [{ value: fieldValue, ...textInputProps }] = useFormikFieldProps({ + fieldID: id, + placeholder: explicitPlaceholder, + variant: explicitVariant, + }); + + return ( + + // TextField props: + value={fieldValue ?? ""} + className={formClassNames.currencyInput} + InputProps={{ + ...InputProps, + inputMode: "numeric", // see jsdoc for why this isn't "number" + startAdornment: ( + + $ + + ), + sx: { + "& input": { textAlign: "right" }, + ...(InputProps?.sx ?? {}), + }, + }} + {...textInputProps} + {...props} + // NumericFormat props: + customInput={BaseTextField} + allowLeadingZeros={false} + allowNegative={false} + decimalScale={2} + fixedDecimalScale + thousandSeparator + valueIsNumericString + /> + ); +}; + +export type CurrencyInputProps = Simplify< + FormikIntegratedInputProps< + Omit< + NumericFormatProps, + | "allowLeadingZeros" + | "allowNegative" + | "autoComplete" + | "customInput" + | "decimalScale" + | "fixedDecimalScale" + | "thousandSeparator" + | "type" + | "valueIsNumericString" + > & { + autoComplete?: "transaction-amount"; + InputProps?: Omit; + } + > +>; diff --git a/src/components/Form/inputs/DatePicker.tsx b/src/components/Form/inputs/DatePicker.tsx new file mode 100644 index 00000000..951080d0 --- /dev/null +++ b/src/components/Form/inputs/DatePicker.tsx @@ -0,0 +1,118 @@ +import { grid as muiGridSxProps, type GridProps as MuiGridSxProps } from "@mui/system"; +import { styled } from "@mui/material/styles"; +import { + DesktopDatePicker, + type DesktopDatePickerProps, +} from "@mui/x-date-pickers/DesktopDatePicker"; +import { MobileDatePicker, type MobileDatePickerProps } from "@mui/x-date-pickers/MobileDatePicker"; +import { getTypeSafeError } from "@/utils/typeSafety/getTypeSafeError"; +import { formClassNames } from "../classNames"; +import { useFormikFieldProps, type FormikIntegratedInputProps } from "../helpers"; +import type { TextFieldProps } from "@mui/material/TextField"; +import type { ConfigType as DayjsInputType } from "dayjs"; +import type { Simplify } from "type-fest"; + +/** + * Mui DatePicker with Formik bindings and Mui-system grid props like `gridArea`. + * If event handler functions like `onChange`/`onOpen` are provided as props to this + * component, they are called after the Formik handlers with all available arguments + * if no errors occur. + * + * ### Layout Responsiveness + * + * If `isMobilePageLayout` from `PageLayoutContext` is `true`, the `MobileDatePicker` + * is rendered - otherwise, the `DesktopDatePicker` is rendered. + * + * > **_Q: Why not use the "responsive" `DatePicker` Mui provides?_** + * > + * > A: The [internal logic Mui uses][mui-dp] for the responsive `DatePicker` is + * based on css media queries using their `useMediaQuery` hook, and that approach + * simply does not achieve the desired behavior for this use case. + * + * ### SlotProps + * + * The `variant` and `style` props, if provided, are passed to the `TextField` slot + * (defaults to `"outlined"` on mobile and `"filled"` on desktop). + * + * @example + * ```tsx + * + * ``` + * + * [mui-dp]: https://github.com/mui/mui-x/blob/next/packages/x-date-pickers/src/DatePicker/DatePicker.tsx + */ +export const DatePicker = ({ + id, + onChange: callerOnChangeHandler, + onOpen: callerOnOpenHandler, + variant: explicitVariant, + style, + slotProps = {}, + className: additionalClassNames = "", + ...props +}: DatePickerProps) => { + const [{ value: fieldValue, variant }, { setValue, setTouched, setError, isMobilePageLayout }] = + useFormikFieldProps({ + fieldID: id, + variant: explicitVariant, + }); + + const handleFieldValueChange: DatePickerProps["onChange"] = async (value, context) => { + try { + await setValue(value); + if (callerOnChangeHandler) await callerOnChangeHandler(value, context); + } catch (error) { + setError(getTypeSafeError(error).message); + } + }; + + const handleOpenPicker: DatePickerProps["onOpen"] = async () => { + try { + await setTouched(true); + if (callerOnOpenHandler) await callerOnOpenHandler(); + } catch (error) { + setError(getTypeSafeError(error).message); + } + }; + + // Props passed to the Mui DesktopDatePicker/MobileDatePicker + const datePickerProps = { + value: fieldValue, + onChange: handleFieldValueChange, + onOpen: handleOpenPicker, + className: `${formClassNames.dateInput} ${additionalClassNames}`, + slotProps: { + textField: { + variant, + style, + ...(slotProps?.textField ?? {}), + }, + ...slotProps, + }, + ...props, + }; + + return isMobilePageLayout ? ( + + ) : ( + + ); +}; + +export const StyledMobileDatePicker = styled(MobileDatePicker, { + shouldForwardProp: (propName: string) => !propName.startsWith("grid"), +})(muiGridSxProps) as typeof MobileDatePicker; + +export const StyledDesktopDatePicker = styled(DesktopDatePicker, { + shouldForwardProp: (propName: string) => !propName.startsWith("grid"), +})(muiGridSxProps) as typeof DesktopDatePicker; + +export type DatePickerProps = Simplify< + FormikIntegratedInputProps< + MobileDatePickerProps & DesktopDatePickerProps, + "onChange" | "onOpen" + > & + Pick & // <-- props passed to the TextField slot + MuiGridSxProps & + React.RefAttributes +>; diff --git a/src/components/Form/inputs/DateTimePicker.tsx b/src/components/Form/inputs/DateTimePicker.tsx new file mode 100644 index 00000000..431172a9 --- /dev/null +++ b/src/components/Form/inputs/DateTimePicker.tsx @@ -0,0 +1,121 @@ +import { grid as muiGridSxProps, type GridProps as MuiGridSxProps } from "@mui/system"; +import { styled } from "@mui/material/styles"; +import { + DesktopDateTimePicker, + type DesktopDateTimePickerProps, +} from "@mui/x-date-pickers/DesktopDateTimePicker"; +import { + MobileDateTimePicker, + type MobileDateTimePickerProps, +} from "@mui/x-date-pickers/MobileDateTimePicker"; +import { getTypeSafeError } from "@/utils/typeSafety/getTypeSafeError"; +import { formClassNames } from "../classNames"; +import { useFormikFieldProps, type FormikIntegratedInputProps } from "../helpers"; +import type { TextFieldProps } from "@mui/material/TextField"; +import type { ConfigType as DayjsInputType } from "dayjs"; +import type { Simplify } from "type-fest"; + +/** + * Mui DateTimePicker with Formik bindings and Mui-system grid props like `gridArea`. + * If event handler functions like `onChange`/`onOpen` are provided as props to this + * component, they are called after the Formik handlers with all available arguments + * if no errors occur. + * + * ### Layout Responsiveness + * + * If `isMobilePageLayout` from `PageLayoutContext` is `true`, the `MobileDateTimePicker` + * is rendered - otherwise, the `DesktopDateTimePicker` is rendered. + * + * > **_Q: Why not use the "responsive" `DateTimePicker` Mui provides?_** + * > + * > A: The [internal logic Mui uses][mui-dtp] for the responsive `DateTimePicker` is + * based on css media queries using their `useMediaQuery` hook, and that approach + * simply does not achieve the desired behavior for this use case. + * + * ### SlotProps + * + * The `variant` and `style` props, if provided, are passed to the `TextField` slot + * (defaults to `"outlined"` on mobile and `"filled"` on desktop). + * + * @example + * ```tsx + * + * ``` + * + * [mui-dtp]: https://github.com/mui/mui-x/blob/next/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.tsx + */ +export const DateTimePicker = ({ + id, + onChange: callerOnChangeHandler, + onOpen: callerOnOpenHandler, + variant: explicitVariant, + style, + slotProps = {}, + className: additionalClassNames = "", + ...props +}: DateTimePickerProps) => { + const [{ value: fieldValue, variant }, { setValue, setTouched, setError, isMobilePageLayout }] = + useFormikFieldProps({ + fieldID: id, + variant: explicitVariant, + }); + + const handleFieldValueChange: DateTimePickerProps["onChange"] = async (value, context) => { + try { + await setValue(value); + if (callerOnChangeHandler) await callerOnChangeHandler(value, context); + } catch (error) { + setError(getTypeSafeError(error).message); + } + }; + + const handleOpenPicker: DateTimePickerProps["onOpen"] = async () => { + try { + await setTouched(true); + if (callerOnOpenHandler) await callerOnOpenHandler(); + } catch (error) { + setError(getTypeSafeError(error).message); + } + }; + + // Props passed to the Mui DesktopDateTimePicker/MobileDateTimePicker + const dateTimePickerProps = { + value: fieldValue, + onChange: handleFieldValueChange, + onOpen: handleOpenPicker, + className: `${formClassNames.dateTimeInput} ${additionalClassNames}`, + slotProps: { + textField: { + variant, + style, + ...(slotProps?.textField ?? {}), + }, + ...slotProps, + }, + ...props, + }; + + return isMobilePageLayout ? ( + + ) : ( + + ); +}; + +export const StyledMobileDateTimePicker = styled(MobileDateTimePicker, { + shouldForwardProp: (propName: string) => !propName.startsWith("grid"), +})(muiGridSxProps) as typeof MobileDateTimePicker; + +export const StyledDesktopDateTimePicker = styled(DesktopDateTimePicker, { + shouldForwardProp: (propName: string) => !propName.startsWith("grid"), +})(muiGridSxProps) as typeof DesktopDateTimePicker; + +export type DateTimePickerProps = Simplify< + FormikIntegratedInputProps< + MobileDateTimePickerProps & DesktopDateTimePickerProps, + "onChange" | "onOpen" + > & + Pick & // <-- props passed to the TextField slot + MuiGridSxProps & + React.RefAttributes +>; diff --git a/src/components/Form/PasswordInput.tsx b/src/components/Form/inputs/PasswordInput.tsx similarity index 75% rename from src/components/Form/PasswordInput.tsx rename to src/components/Form/inputs/PasswordInput.tsx index f8326a1c..7fa2df77 100644 --- a/src/components/Form/PasswordInput.tsx +++ b/src/components/Form/inputs/PasswordInput.tsx @@ -4,7 +4,8 @@ import InputAdornment from "@mui/material/InputAdornment"; import Visibility from "@mui/icons-material/Visibility"; import VisibilityOff from "@mui/icons-material/VisibilityOff"; import { TextInput, type TextInputProps } from "./TextInput"; -import { formClassNames } from "./classNames"; +import { formClassNames } from "../classNames"; +import type { OverrideProperties } from "type-fest"; export const PasswordInput = ({ InputProps = {}, ...props }: PasswordInputProps) => { const [showPassword, setShowPassword] = useState(false); @@ -24,9 +25,9 @@ export const PasswordInput = ({ InputProps = {}, ...props }: PasswordInputProps) {showPassword ? ( - + ) : ( - + )} @@ -43,7 +44,10 @@ export const PasswordInput = ({ InputProps = {}, ...props }: PasswordInputProps) * - `autoComplete` is restricted to only the values that are valid for password inputs * - `type` and `InputProps["endAdornment"]` are removed since they're handled internally */ -export type PasswordInputProps = Omit & { - autoComplete?: "current-password" | "new-password"; - InputProps?: Omit; -}; +export type PasswordInputProps = OverrideProperties< + Omit, + { + autoComplete?: "current-password" | "new-password"; + InputProps?: Omit; + } +>; diff --git a/src/components/Form/inputs/PhoneInput.tsx b/src/components/Form/inputs/PhoneInput.tsx new file mode 100644 index 00000000..f1bbac45 --- /dev/null +++ b/src/components/Form/inputs/PhoneInput.tsx @@ -0,0 +1,49 @@ +import { PatternFormat, type PatternFormatProps } from "react-number-format"; +import { BaseTextField, type BaseTextFieldProps } from "./BaseTextField"; +import { formClassNames } from "../classNames"; +import { + useFormikFieldProps, + type FormikIntegratedInputProps, +} from "../helpers/useFormikFieldProps"; + +/** + * TextInput which uses [react-number-format] for phone formatting. + * + * > Currently only the US phone format is supported. + * + * [rnf-docs]: https://s-yadav.github.io/react-number-format/docs/pattern_format + */ +export const PhoneInput = ({ + id, + placeholder: explicitPlaceholder = "(123) 456-7890", + variant: explicitVariant, + ...props +}: PhoneInputProps) => { + const [{ value: fieldValue, ...textInputProps }] = useFormikFieldProps({ + fieldID: id, + placeholder: explicitPlaceholder, + variant: explicitVariant, + }); + + return ( + + // TextField props: + value={fieldValue ?? ""} + type="tel" + autoComplete="tel" + className={formClassNames.phoneInput} + {...textInputProps} + {...props} + // PatternFormat props: + customInput={BaseTextField} + format="(###) ### - ####" + /> + ); +}; + +export type PhoneInputProps = FormikIntegratedInputProps< + Omit< + PatternFormatProps, + "type" | "autoComplete" | "customInput" | "format" | "valueIsNumericString" + > +>; diff --git a/src/components/Form/inputs/RegionInput.tsx b/src/components/Form/inputs/RegionInput.tsx new file mode 100644 index 00000000..e679553c --- /dev/null +++ b/src/components/Form/inputs/RegionInput.tsx @@ -0,0 +1,30 @@ +import { useField } from "formik"; +import { AutoCompleteStates } from "./AutoCompleteStates"; +import { TextInput } from "./TextInput"; + +/** + * This Formik-integrated input is used to gather a location's _region_. + * The rendered component depends on the location's _country_ value: + * + * - When a location's _country_ value is `"USA"`, an {@link AutoCompleteStates} + * component which includes a comprehensive list of US states and territories. + * + * - For any other _country_, this component renders a {@link TextInput} component + * to provide the user with the flexibility to input any value. + * + * @param countryFieldID The Formik field ID for the _country_ value, e.g. `'location["country"]'`. + */ +export const RegionInput = ({ countryFieldID, regionFieldID }: RegionInputProps) => { + const [{ value: countryValue }] = useField(countryFieldID); + + return countryValue === "USA" || !countryValue ? ( + + ) : ( + + ); +}; + +export type RegionInputProps = { + countryFieldID: string; + regionFieldID: string; +}; diff --git a/src/components/Form/inputs/Select.tsx b/src/components/Form/inputs/Select.tsx new file mode 100644 index 00000000..75143fc0 --- /dev/null +++ b/src/components/Form/inputs/Select.tsx @@ -0,0 +1,78 @@ +import FormControl, { type FormControlProps } from "@mui/material/FormControl"; +import FormHelperText from "@mui/material/FormHelperText"; +import InputLabel from "@mui/material/InputLabel"; +import MenuItem from "@mui/material/MenuItem"; +import MuiSelect from "@mui/material/Select"; +import { useFormikFieldProps, type FormikIntegratedInputProps } from "../helpers"; +import type { SelectProps as MuiSelectProps } from "@mui/material/Select"; + +export const Select = < + ValueType extends string | number | null | undefined = string | number | null, +>({ + id, + options, + label, + variant: explicitVariant, + fullWidth = false, + sx, + FormControlProps = {}, + ...muiSelectProps +}: SelectProps) => { + const [ + { value: fieldValue, variant, error: showErrorState, helperText }, + { setValue, setError }, + ] = useFormikFieldProps({ + fieldID: id, + variant: explicitVariant, + }); + + const handleChange: SelectProps["onChange"] = async (event) => { + await setValue(event.target.value as ValueType).catch((error) => setError(error)); + }; + + const selectLabelID = `Select:InputLabel:${id}`; + + return ( + + {label ?? id} + + {options.map(({ value, label }, index) => ( + + {label ?? value} + + ))} + + {helperText} + + ); +}; + +export type SelectProps< + ValueType extends string | number | null | undefined = string | number | null, +> = FormikIntegratedInputProps< + { + label?: React.ReactNode; + options: SelectOptions; + sx?: FormControlProps["sx"]; + FormControlProps?: Omit; + } & Omit +>; + +export type SelectOptions< + ValueType extends string | number | null | undefined = string | number | null, +> = Array<{ + value: ValueType; + label?: string; +}>; diff --git a/src/components/Form/Slider.tsx b/src/components/Form/inputs/Slider.tsx similarity index 69% rename from src/components/Form/Slider.tsx rename to src/components/Form/inputs/Slider.tsx index d5e6ebcb..f0f86f8d 100644 --- a/src/components/Form/Slider.tsx +++ b/src/components/Form/inputs/Slider.tsx @@ -1,13 +1,16 @@ -import { useFormikContext } from "formik"; import { grid as muiGridSxProps, type GridProps as MuiGridSxProps } from "@mui/system"; import { styled } from "@mui/material/styles"; import Box from "@mui/material/Box"; import InputLabel from "@mui/material/InputLabel"; import MuiSlider, { sliderClasses, type SliderProps as MuiSliderProps } from "@mui/material/Slider"; -import { formClassNames } from "./classNames"; +import { formClassNames } from "../classNames"; +import { + useFormikFieldProps, + type FormikIntegratedInputProps, +} from "../helpers/useFormikFieldProps"; /** - * MUI Slider with Formik hooks + * MUI Slider with Formik integration. * * - `getFieldValue`: Since the "value" property of discrete mark options must * be numbers, the `getFieldValue` fn serves as a hook which can be used to @@ -15,9 +18,9 @@ import { formClassNames } from "./classNames"; * whatever type/value is desired for the form. The value returned from * `getFieldValue` is provided to the field's Formik-context value. * - * - The Mui `sx` prop is passed to the containing div (a Mui Box). + * - The Mui `sx` prop is passed to the containiner - a Mui Box. */ -export const Slider = ({ +export const Slider = ({ id, label, getFieldValue = (value) => value, @@ -25,11 +28,11 @@ export const Slider = ({ style, ...props }: SliderProps) => { - const { setFieldValue } = useFormikContext(); + const [_, { setValue, setError }] = useFormikFieldProps({ fieldID: id }); - const handleChange = (event: Event, value: number | Array, _activeThumb: number) => { + const handleChange = (_event: Event, value: number | Array, _activeThumb: number) => { const fieldValue = getFieldValue(value); - setFieldValue(id, fieldValue); + setValue(fieldValue as any).catch((error) => setError(error)); }; const labelID = `Slider:InputLabel:${id}`; @@ -59,10 +62,10 @@ export const Slider = ({ */ const StyledMuiSlider = styled(MuiSlider, { shouldForwardProp: (propName: string) => !propName.startsWith("grid"), -})(({ theme }) => ({ +})(({ theme: { palette } }) => ({ height: "10px", marginBottom: "0.5rem", - color: theme.palette.primary.dark, + color: palette.primary.dark, [`& .${sliderClasses.track}`]: { border: "none", @@ -71,7 +74,7 @@ const StyledMuiSlider = styled(MuiSlider, { [`& .${sliderClasses.thumb}`]: { height: "1.5rem", width: "1.5rem", - backgroundColor: theme.palette.primary.main, + backgroundColor: palette.primary.main, border: "2px solid currentColor", [`&:focus, &:hover, &.${sliderClasses.active}, &.${sliderClasses.focusVisible}`]: { @@ -83,14 +86,14 @@ const StyledMuiSlider = styled(MuiSlider, { }, [`& .${sliderClasses.valueLabel}`]: { - lineHeight: 1.2, - fontSize: 12, + // lineHeight: 1.2, // TODO rm this line if text is fine after setting default line-height:1.5 + fontSize: "12px", background: "unset", padding: 0, width: "2rem", height: "2rem", borderRadius: "50% 50% 50% 0", - backgroundColor: theme.palette.primary.dark, + backgroundColor: palette.primary.dark, transformOrigin: "bottom left", transform: "translate(50%, -100%) rotate(-45deg) scale(0)", "&::before": { @@ -106,15 +109,16 @@ const StyledMuiSlider = styled(MuiSlider, { [`& .${sliderClasses.markLabel}`]: { marginTop: "3px", - color: theme.palette.text.primary, + color: palette.text.primary, fontWeight: "light", }, ...muiGridSxProps, })); -export type SliderProps = MuiSliderProps & { - id: string; - label: string; - getFieldValue?: (value: number | Array) => any; -} & MuiGridSxProps; +export type SliderProps = FormikIntegratedInputProps< + MuiSliderProps & { + label: string; + getFieldValue?: (value: number | Array) => number | number[] | string | string[]; + } & MuiGridSxProps +>; diff --git a/src/components/Form/inputs/SliderWorkOrderPriority.tsx b/src/components/Form/inputs/SliderWorkOrderPriority.tsx new file mode 100644 index 00000000..a47d7920 --- /dev/null +++ b/src/components/Form/inputs/SliderWorkOrderPriority.tsx @@ -0,0 +1,55 @@ +import { Slider, type SliderProps } from "@/components/Form/inputs/Slider"; +import { capitalize } from "@/utils/formatters/strings"; +import type { Mark } from "@mui/base/useSlider"; + +/** + * WorkOrder: SelectPriority (MUI Slider) + */ +export const SliderWorkOrderPriority = ({ + id, + label, + ...sliderProps +}: SliderWorkOrderPriorityProps) => ( + +); + +const PRIORITY_OPTIONS = [ + { value: 7, label: "LOW" }, + { value: 50, label: "NORMAL" }, + { value: 93, label: "HIGH" }, +] as const satisfies Mark[]; + +const getFieldValue = (value: number | number[]) => { + return PRIORITY_OPTIONS.find((opt) => opt.value === value)?.label ?? "NORMAL"; +}; + +const valueLabelFormat = (value: number) => { + const optLabel = PRIORITY_OPTIONS.find((opt) => opt.value === value)?.label; + return optLabel ? capitalize(optLabel) : "Priority"; +}; + +export type SliderWorkOrderPriorityProps = Omit< + SliderProps, + | "marks" + | "getFieldValue" + | "valueLabelFormat" + | "valueLabelDisplay" + | "min" + | "max" + | "defaultValue" + | "step" + | "track" +>; diff --git a/src/components/Form/inputs/TextInput.tsx b/src/components/Form/inputs/TextInput.tsx new file mode 100644 index 00000000..fc091286 --- /dev/null +++ b/src/components/Form/inputs/TextInput.tsx @@ -0,0 +1,88 @@ +import { BaseTextField, type BaseTextFieldProps } from "./BaseTextField"; +import { formClassNames } from "../classNames"; +import { useFormikFieldProps, type FormikIntegratedInputProps } from "../helpers"; + +export const TextInput = ({ + id, + label: explicitLabel, + placeholder: explicitPlaceholder, + variant: explicitVariant, + ...props +}: TextInputProps) => { + const [{ value, ...textInputProps }] = useFormikFieldProps({ + fieldID: id, + label: explicitLabel, + placeholder: explicitPlaceholder, + variant: explicitVariant, + }); + + return ( + + ); +}; + +/** + * {@link TextInput} props + * + * > Per [Mui's recommendation][mui-docs], this component does not support `type="number"`. + * + * [mui-docs]: https://mui.com/material-ui/react-text-field/#type-quot-number-quot + */ +export type TextInputProps = FormikIntegratedInputProps< + Omit< + BaseTextFieldProps, + // These props are removed bc they're handled internally by TextInput's Formik integration: + | "onChange" + | "onBlur" + // These props are overridden in the intersection below: + | "label" + | "autoComplete" + | "type" + > & { + label?: string; + autoComplete?: AutoCompleteAttributeValue; + type?: Exclude["type"], "number">; + } +>; + +/** + * Union typing of supported `autocomplete` attribute values. + * See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete + * + * Note: this type does not reflect an exhaustive list of all possible `autocomplete` + * values; certain values have been excluded for i18n purposes if they reflect only a + * region-specific consitutent part of a data point for which a more generalized form + * is both available and preferred (for example, "name" is preferred over "given-name", + * "family-name", "honorific-prefix", "honorific-suffix", etc.). + */ +export type AutoCompleteAttributeValue = + // PERSONAL + | "name" + | "email" + | "tel" + // BUSINESS + | "organization" + | "organization-title" + // ADDRESS + | "street-address" + | "postal-code" // in the US this is a ZIP code + | "country" // country or territyory CODE + | "country-name" // country or territyory NAME + // AUTHENTICATION + | "username" + | "current-password" + | "new-password" + | "one-time-code" + // PAYMENT + | "cc-name" + | "cc-number" + | "cc-exp" // MM/YY or MM/YYYY + | "cc-csc" // payment security code + | "cc-type" // payment card typed (e.g. "Visa") + | "transaction-currency" + | "transaction-amount"; diff --git a/src/components/Form/inputs/index.ts b/src/components/Form/inputs/index.ts new file mode 100644 index 00000000..75d5eabc --- /dev/null +++ b/src/components/Form/inputs/index.ts @@ -0,0 +1,18 @@ +export * from "./AutoComplete"; +export * from "./AutoCompleteContact"; +export * from "./AutoCompleteMyContacts"; +export * from "./AutoCompleteStates"; +export * from "./AutoCompleteWorkOrder"; +export * from "./AutoCompleteWorkOrderCategory"; +export * from "./BaseTextField"; +export * from "./ChecklistInput"; +export * from "./CurrencyInput"; +export * from "./DatePicker"; +export * from "./DateTimePicker"; +export * from "./PasswordInput"; +export * from "./PhoneInput"; +export * from "./RegionInput"; +export * from "./Select"; +export * from "./Slider"; +export * from "./SliderWorkOrderPriority"; +export * from "./TextInput"; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts deleted file mode 100644 index b01d4b3c..00000000 --- a/src/components/Form/types.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Union typing of supported `autocomplete` attribute values. - * See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete - * - * Note: this type does not reflect an exhaustive list of all possible `autocomplete` - * values; certain values have been excluded for i18n purposes if they reflect only a - * region-specific consitutent part of a data point for which a more generalized form - * is both available and preferred (for example, "name" is preferred over "given-name", - * "family-name", "honorific-prefix", "honorific-suffix", etc.). - */ -export type AutoCompleteAttributeValue = - // PERSONAL - | "name" - | "email" - | "tel" - // BUSINESS - | "organization" - | "organization-title" - // ADDRESS - | "street-address" - | "postal-code" // in the US this is a ZIP code - | "country" // country or territyory CODE - | "country-name" // country or territyory NAME - // AUTHENTICATION - | "username" - | "current-password" - | "new-password" - | "one-time-code" - // PAYMENT - | "cc-name" - | "cc-number" - | "cc-exp" // MM/YY or MM/YYYY - | "cc-csc" // payment security code - | "cc-type" // payment card typed (e.g. "Visa") - | "transaction-currency" - | "transaction-amount"; diff --git a/src/components/Form/useDefaultTextFieldVariant.ts b/src/components/Form/useDefaultTextFieldVariant.ts deleted file mode 100644 index dd27d84c..00000000 --- a/src/components/Form/useDefaultTextFieldVariant.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { usePageLayoutContext } from "@app/PageLayoutContext/usePageLayoutContext"; -import type { TextFieldProps } from "@mui/material/TextField"; - -/** - * Default `variant` for all Mui inputs: - * - * - "filled" on desktop - * - "outlined" on mobile - */ -export const useDefaultTextFieldVariant = (): TextFieldProps["variant"] => { - const { isMobilePageLayout } = usePageLayoutContext(); - return isMobilePageLayout ? "outlined" : "filled"; -}; diff --git a/src/components/Form/useFormikFieldProps.ts b/src/components/Form/useFormikFieldProps.ts deleted file mode 100644 index efff9c7d..00000000 --- a/src/components/Form/useFormikFieldProps.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { useField, type FieldInputProps, type FieldMetaProps, type FieldHelperProps } from "formik"; -import { useDefaultTextFieldVariant } from "./useDefaultTextFieldVariant"; -import type { TextFieldProps as MuiTextFieldProps } from "@mui/material/TextField"; - -/** - * Defines shared logical behavior for Mui inputs and related/derived components. - * - * @template ValueType - The type of the value being managed by the input. - * - * @param {boolean=} [shouldAlwaysRenderHelperText=true] - A bool switch which when `true` - * will cause Mui HelperText to always be rendered, even when empty/undefined. This - * is useful when the Mui input is used in a layout where the conditional rendering - * of the HelperText would cause layout changes when the input is in an error state. - * - * @returns A tuple containing two objects: - * - `index[0]`: MuiTextFieldProps, this object can be provided as-is to most Mui inputs. - * - `index[1]`: Contains all original values provided by the Formik `useField` hook. - */ -export const useFormikFieldProps = ({ - id, - label: explicitLabel, - variant: explicitVariant, - placeholder, - shouldAlwaysRenderHelperText = true, -}: UseFormikFieldPropsParams): UseFormikFieldPropsReturn => { - const [fieldInputProps, fieldMetaProps, fieldHelperProps] = useField(id); - - const { value: fieldValue, onChange, onBlur } = fieldInputProps; - - const defaultVariant = useDefaultTextFieldVariant(); - - const label = explicitLabel ?? id; - const showErrorState = fieldMetaProps.touched && !!fieldMetaProps?.error; - - return [ - // Props for Mui TextField and other inputs: - { - id, - label, - ...(!showErrorState && placeholder && { placeholder }), - variant: explicitVariant ?? defaultVariant, - value: fieldValue, - onChange, - onBlur, - error: showErrorState, - helperText: showErrorState - ? `${fieldMetaProps.error}` - : shouldAlwaysRenderHelperText - ? " " - : "", - }, - // Original Formik useField values: - { - ...fieldInputProps, - ...fieldMetaProps, - ...fieldHelperProps, - }, - ]; -}; - -export type UseFormikFieldPropsParams = { - id: string; - label?: string; - variant?: MuiTextFieldProps["variant"]; - placeholder?: string; - shouldAlwaysRenderHelperText?: boolean; -}; - -export type UseFormikFieldPropsReturn = [ - { - id: string; - label: string; - placeholder?: string; - variant: MuiTextFieldProps["variant"]; - value: ValueType; - onChange: FieldInputProps["onChange"]; - onBlur: FieldInputProps["onBlur"]; - error: boolean; - helperText: string; - }, - FieldInputProps & FieldMetaProps & FieldHelperProps -];