From d3a68b0907c0569009814b433601e3ec260e9d28 Mon Sep 17 00:00:00 2001 From: Bruno Henriques Date: Mon, 25 Nov 2024 15:42:51 +0000 Subject: [PATCH] fix(FormElement): uniformize prop > context fallbacks chore: dedupe props & initial states --- .../core/src/BaseCheckBox/BaseCheckBox.tsx | 47 +----- packages/core/src/BaseInput/BaseInput.tsx | 16 +- packages/core/src/BaseInput/validations.ts | 20 +-- packages/core/src/Calendar/Calendar.tsx | 24 +-- .../CalendarHeader/CalendarHeader.tsx | 10 +- packages/core/src/CheckBox/CheckBox.tsx | 10 +- .../core/src/CheckBoxGroup/CheckBoxGroup.tsx | 68 ++------ packages/core/src/ColorPicker/ColorPicker.tsx | 37 ++--- packages/core/src/DatePicker/DatePicker.tsx | 8 +- packages/core/src/Dropdown/Dropdown.tsx | 6 +- packages/core/src/FilterGroup/FilterGroup.tsx | 31 +--- .../src/FormElement/Adornment/Adornment.tsx | 16 +- .../FormElement/CharCounter/CharCounter.tsx | 28 ++-- .../core/src/FormElement/FormElement.test.tsx | 27 ++- packages/core/src/FormElement/FormElement.tsx | 58 +++---- .../FormElement/InfoMessage/InfoMessage.tsx | 6 +- packages/core/src/FormElement/Label/Label.tsx | 22 ++- .../FormElement/Suggestions/Suggestions.tsx | 4 +- .../FormElement/WarningText/WarningText.tsx | 9 +- packages/core/src/FormElement/context.ts | 31 +++- packages/core/src/FormElement/utils.ts | 10 +- packages/core/src/Input/Input.tsx | 26 ++- packages/core/src/Radio/Radio.tsx | 13 +- packages/core/src/RadioGroup/RadioGroup.tsx | 6 +- .../core/src/SelectionList/SelectionList.tsx | 6 +- packages/core/src/Slider/Slider.tsx | 6 +- packages/core/src/Switch/Switch.tsx | 11 +- packages/core/src/TagsInput/TagsInput.tsx | 19 ++- .../core/src/TextArea/TextArea.stories.tsx | 155 +----------------- packages/core/src/TextArea/TextArea.tsx | 18 +- packages/core/src/TimePicker/TimePicker.tsx | 6 +- packages/core/src/index.ts | 1 - packages/core/src/types/forms.ts | 20 --- 33 files changed, 247 insertions(+), 528 deletions(-) delete mode 100644 packages/core/src/types/forms.ts diff --git a/packages/core/src/BaseCheckBox/BaseCheckBox.tsx b/packages/core/src/BaseCheckBox/BaseCheckBox.tsx index 78f914b927..843c5a0ed6 100644 --- a/packages/core/src/BaseCheckBox/BaseCheckBox.tsx +++ b/packages/core/src/BaseCheckBox/BaseCheckBox.tsx @@ -16,10 +16,6 @@ export type HvBaseCheckBoxClasses = ExtractNames; export interface HvBaseCheckBoxProps extends Omit { - /** - * The input name. - */ - name?: string; /** * The value of the input. * @@ -29,32 +25,6 @@ export interface HvBaseCheckBoxProps * The default value is "on". */ value?: any; - /** - * Indicates that the input is disabled. - */ - disabled?: boolean; - /** - * Indicates that the input is not editable. - */ - readOnly?: boolean; - /** - * Indicates that user input is required. - */ - required?: boolean; - /** - * If `true` the checkbox is selected, if set to `false` the checkbox is not selected. - * - * When defined the checkbox state becomes controlled. - */ - checked?: boolean; - /** - * When uncontrolled, defines the initial checked state. - */ - defaultChecked?: boolean; - /** - * If `true` the checkbox visually shows the indeterminate state. - */ - indeterminate?: boolean; /** * The callback fired when the checkbox is pressed. */ @@ -63,23 +33,8 @@ export interface HvBaseCheckBoxProps checked: boolean, value: any, ) => void; - /** - * Whether the selector should use semantic colors. - */ + /** Whether the selector should use semantic colors. */ semantic?: boolean; - /** - * Properties passed on to the input element. - */ - inputProps?: React.InputHTMLAttributes; - /** - * Callback fired when the component is focused with a keyboard. - * We trigger a `onFocus` callback too. - */ - onFocusVisible?: (event: React.FocusEvent) => void; - /** - * Callback fired when the component is blurred. - */ - onBlur?: (event: React.FocusEvent) => void; /** * A Jss Object used to override or extend the styles applied to the checkbox. */ diff --git a/packages/core/src/BaseInput/BaseInput.tsx b/packages/core/src/BaseInput/BaseInput.tsx index 32394a9afd..2192bfe464 100644 --- a/packages/core/src/BaseInput/BaseInput.tsx +++ b/packages/core/src/BaseInput/BaseInput.tsx @@ -54,26 +54,16 @@ const baseInputStyles = emotionCss({ export interface HvBaseInputProps extends Omit { - /** The input name. */ - name?: string; /** The value of the input, when controlled. */ value?: string; /** The initial value of the input, when uncontrolled. */ defaultValue?: string; - /** If `true` the input is disabled. */ - disabled?: boolean; - /** Indicates that the input is not editable. */ - readOnly?: boolean; - /** If true, the input element will be required. */ - required?: boolean; /** The function that will be executed onChange, allows modification of the input, * it receives the value. If a new value should be presented it must returned it. */ onChange?: ( event: React.ChangeEvent, value: string, ) => void; - /** The input type. */ - type?: string; /** Label inside the input used to help user. */ placeholder?: string; /** If true, a textarea element will be rendered. */ @@ -112,9 +102,9 @@ export const HvBaseInput = forwardRef< onChange, type = "text", placeholder, - multiline = false, - resizable = false, - invalid = false, + multiline, + resizable, + invalid, inputRef, inputProps = {}, ...others diff --git a/packages/core/src/BaseInput/validations.ts b/packages/core/src/BaseInput/validations.ts index 2ecbf8c6fb..1cd6fa544b 100644 --- a/packages/core/src/BaseInput/validations.ts +++ b/packages/core/src/BaseInput/validations.ts @@ -37,7 +37,7 @@ export const computeValidationType = (type: React.HTMLInputTypeAttribute) => { * Checks whether any integrated validation, native or not, is active. */ export const hasBuiltInValidations = ( - required: boolean, + required: boolean | undefined, validationType: React.HTMLInputTypeAttribute, minCharQuantity: number | null | undefined, maxCharQuantity: number | null | undefined, @@ -119,7 +119,7 @@ export const computeValidationMessage = ( export const validateInput = ( input: HTMLInputElement | HTMLTextAreaElement | null, value: string, - required: boolean, + required: boolean | undefined, minCharQuantity: any, maxCharQuantity: any, validationType: string, @@ -188,19 +188,9 @@ export const validateInput = ( return inputValidity; }; -export type HvInputValidity = { - valid?: boolean; - badInput?: boolean; - customError?: boolean; - patternMismatch?: boolean; - rangeOverflow?: boolean; - rangeUnderflow?: boolean; - stepMismatch?: boolean; - tooLong?: boolean; - tooShort?: boolean; - typeMismatch?: boolean; - valueMissing?: boolean; -}; +type Mutable = { -readonly [P in keyof T]: T[P] }; + +export interface HvInputValidity extends Partial> {} export const DEFAULT_ERROR_MESSAGES = { error: "Invalid value", diff --git a/packages/core/src/Calendar/Calendar.tsx b/packages/core/src/Calendar/Calendar.tsx index 8ce08b9eab..7fe7843619 100644 --- a/packages/core/src/Calendar/Calendar.tsx +++ b/packages/core/src/Calendar/Calendar.tsx @@ -112,9 +112,9 @@ export interface HvCalendarProps { export const HvCalendar = (props: HvCalendarProps) => { const { classes: classesProp, - id, + id: idProp, locale = "en-US", - value, + value: valueProp, visibleMonth, visibleYear, rightVisibleMonth, @@ -130,20 +130,20 @@ export const HvCalendar = (props: HvCalendarProps) => { } = useDefaultProps("HvCalendar", props); const { classes } = useClasses(classesProp); - const { elementId } = useContext(HvFormElementContext); + const context = useContext(HvFormElementContext); const elementValue = useContext(HvFormElementValueContext); - const localValue = value ?? elementValue; - const localId = id ?? setId(elementId, "single-calendar"); - const rangeMode = isRange(localValue); - const rightCalendarId = setId(localId, "single-calendar-right"); + const value = valueProp ?? elementValue; + const id = idProp ?? setId(context.id, "single-calendar"); + const rangeMode = isRange(value); + const rightCalendarId = setId(id, "single-calendar-right"); const clampedMonth = visibleMonth && visibleMonth % 13 > 0 ? visibleMonth % 13 : 1; const singleCalendar = ( {
{ className={classes.singleCalendar} id={rightCalendarId} locale={locale} - value={localValue} + value={value} visibleMonth={rightVisibleMonth} visibleYear={rightVisibleYear} minimumDate={minimumDate} diff --git a/packages/core/src/Calendar/CalendarHeader/CalendarHeader.tsx b/packages/core/src/Calendar/CalendarHeader/CalendarHeader.tsx index 25adc12255..71455d118f 100644 --- a/packages/core/src/Calendar/CalendarHeader/CalendarHeader.tsx +++ b/packages/core/src/Calendar/CalendarHeader/CalendarHeader.tsx @@ -31,7 +31,7 @@ dayjs.extend(customParseFormat); export const HvCalendarHeader = (props: HvCalendarHeaderProps) => { const { - id, + id: idProp, value, locale = "en-US", classes: classesProp, @@ -44,7 +44,7 @@ export const HvCalendarHeader = (props: HvCalendarHeaderProps) => { const { classes, cx } = useClasses(classesProp); - const { elementId } = useContext(HvFormElementContext); + const context = useContext(HvFormElementContext); const elementValue = useContext(HvFormElementValueContext); const { label } = useContext(HvFormElementDescriptorsContext); @@ -61,7 +61,7 @@ export const HvCalendarHeader = (props: HvCalendarHeaderProps) => { const [displayValue, setDisplayValue] = useState(""); const [weekdayDisplay, setWeekdayDisplay] = useState(""); - const localId = id ?? setId(elementId, "calendarHeader"); + const id = idProp ?? setId(context.id, "calendarHeader"); const inputValue = editedValue ?? displayValue; const localeFormat = dayjs().locale(locale).localeData().longDateFormat("L"); @@ -151,7 +151,7 @@ export const HvCalendarHeader = (props: HvCalendarHeaderProps) => { // In a new major there's no need for all these classes return (
{ )} ( labelProps, inputProps, value = "on", - required = false, - readOnly = false, - disabled = false, - semantic = false, - defaultChecked = false, + required, + readOnly, + disabled, + semantic, + defaultChecked, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, diff --git a/packages/core/src/CheckBoxGroup/CheckBoxGroup.tsx b/packages/core/src/CheckBoxGroup/CheckBoxGroup.tsx index 6076609664..a06bd71cb4 100644 --- a/packages/core/src/CheckBoxGroup/CheckBoxGroup.tsx +++ b/packages/core/src/CheckBoxGroup/CheckBoxGroup.tsx @@ -14,6 +14,7 @@ import { import { HvCheckBox } from "../CheckBox"; import { HvFormElement, + HvFormElementProps, HvFormStatus, HvInfoMessage, HvLabel, @@ -58,57 +59,22 @@ export { staticClasses as checkBoxGroupClasses }; export type HvCheckBoxGroupClasses = ExtractNames; export interface HvCheckBoxGroupProps - extends HvBaseProps { - /** - * The form element name. - * - * It is propagated to the children checkboxes, unless they already have one. - */ - name?: string; - /** - * The value of the form element. An array of values represented in the child checkboxes. - * - * When defined the checkbox group state becomes controlled. - */ - value?: any[]; + extends Pick< + HvFormElementProps, + | "name" + | "value" + | "label" + | "description" + | "disabled" + | "required" + | "readOnly" + | "status" + >, + HvBaseProps { /** * When uncontrolled, defines the initial value. */ defaultValue?: any[]; - /** - * The label of the form element. - * - * The form element must be labeled for accessibility reasons. - * If not provided, an aria-label or aria-labelledby must be provided instead. - */ - label?: React.ReactNode; - /** - * Provide additional descriptive text for the form element. - */ - description?: React.ReactNode; - /** - * Indicates that the form element is disabled. - * If `true` the state is propagated to the children checkboxes. - */ - disabled?: boolean; - /** - * Indicates that the form element is not editable. - * If `true` the state is propagated to the children checkboxes. - */ - readOnly?: boolean; - /** - * Indicates that user input is required on the form element. - */ - required?: boolean; - /** - * The status of the form element. - * - * Valid is correct, invalid is incorrect and standBy means no validations have run. - * - * When uncontrolled and unspecified it will default to "standBy" and change to either "valid" - * or "invalid" after any change to the state. - */ - status?: HvFormStatus; /** * The error message to show when the validation status is "invalid". * @@ -160,10 +126,10 @@ export const HvCheckBoxGroup = forwardRef( statusMessage, defaultValue, value: valueProp, - required = false, - readOnly = false, - disabled = false, - showSelectAll = false, + required, + readOnly, + disabled, + showSelectAll, orientation = "vertical", selectAllLabel = "All", selectAllConjunctionLabel = "/", diff --git a/packages/core/src/ColorPicker/ColorPicker.tsx b/packages/core/src/ColorPicker/ColorPicker.tsx index f32de62bf6..44e22bf575 100644 --- a/packages/core/src/ColorPicker/ColorPicker.tsx +++ b/packages/core/src/ColorPicker/ColorPicker.tsx @@ -8,7 +8,12 @@ import { import { HvBaseDropdown } from "../BaseDropdown"; import { HvDropdownProps } from "../Dropdown"; -import { HvFormElement, HvInfoMessage, HvLabel } from "../FormElement"; +import { + HvFormElement, + HvFormElementProps, + HvInfoMessage, + HvLabel, +} from "../FormElement"; import { useControlled } from "../hooks/useControlled"; import { useLabels } from "../hooks/useLabels"; import { useUniqueId } from "../hooks/useUniqueId"; @@ -24,27 +29,16 @@ export { staticClasses as colorPickerClasses }; export type HvColorPickerClasses = ExtractNames; -export interface HvColorPickerProps { +export interface HvColorPickerProps + extends Pick< + HvFormElementProps, + "id" | "name" | "label" | "description" | "disabled" | "required" + > { "aria-label"?: string; "aria-labelledby"?: string; "aria-describedby"?: string; /** Class names to be applied. */ className?: string; - /** Id to be applied to the form element root node. */ - id?: string; - /** The form element name. */ - name?: string; - /** - * The label of the form element. - * - * The form element must be labeled for accessibility reasons. - * If not provided, an aria-label or aria-labelledby must be provided instead. - */ - label?: React.ReactNode; - /** Provide additional descriptive text for the form element. */ - description?: React.ReactNode; - /** Indicates that user input is required on the form element. */ - required?: boolean; /** The value color, in HEX format. */ value?: string; /** The default value color, in HEX format. */ @@ -107,8 +101,8 @@ export const HvColorPicker = forwardRef( const { id, name, - required = false, - disabled = false, + required, + disabled, label, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, @@ -156,10 +150,7 @@ export const HvColorPicker = forwardRef( const labels = useLabels(DEFAULT_LABELS, labelsProp); - const [isOpen, setIsOpen] = useControlled( - expanded, - Boolean(defaultExpanded), - ); + const [isOpen, setIsOpen] = useControlled(expanded, defaultExpanded); const [color, setColor] = useControlled(value, defaultValue); const [savedColors, setSavedColors] = useControlled( savedColorsValue, diff --git a/packages/core/src/DatePicker/DatePicker.tsx b/packages/core/src/DatePicker/DatePicker.tsx index c5a48e90bf..8f3de48247 100644 --- a/packages/core/src/DatePicker/DatePicker.tsx +++ b/packages/core/src/DatePicker/DatePicker.tsx @@ -149,8 +149,8 @@ export const HvDatePicker = forwardRef( id, name, - required = false, - disabled = false, + required, + disabled, readOnly, label, @@ -181,8 +181,8 @@ export const HvDatePicker = forwardRef( startAdornment, horizontalPlacement = "right", locale: localeProp, - showActions = false, - showClear = false, + showActions, + showClear, disablePortal = true, escapeWithReference = true, dropdownProps = {}, diff --git a/packages/core/src/Dropdown/Dropdown.tsx b/packages/core/src/Dropdown/Dropdown.tsx index ea623b4f76..e328cd9d43 100644 --- a/packages/core/src/Dropdown/Dropdown.tsx +++ b/packages/core/src/Dropdown/Dropdown.tsx @@ -196,9 +196,9 @@ export const HvDropdown = fixedForwardRef(function HvDropdown< id, name, - required = false, - disabled = false, - readOnly = false, + required, + disabled, + readOnly, label, "aria-label": ariaLabel, diff --git a/packages/core/src/FilterGroup/FilterGroup.tsx b/packages/core/src/FilterGroup/FilterGroup.tsx index fb4b914357..051af5048d 100644 --- a/packages/core/src/FilterGroup/FilterGroup.tsx +++ b/packages/core/src/FilterGroup/FilterGroup.tsx @@ -7,7 +7,6 @@ import { import { HvFormElement, HvFormElementProps, - HvFormStatus, HvInfoMessage, HvLabel, HvWarningText, @@ -34,35 +33,11 @@ export type HvFilterGroupClasses = ExtractNames; export interface HvFilterGroupProps extends Omit< - HvFormElementProps, + HvFormElementProps, "classes" | "onChange" | "defaultValue" | "statusMessage" > { /** The initial value of the input when in single calendar mode. */ filters: HvFilterGroupFilters; - /** The form element name. */ - name?: string; - /** - * The label of the form element. - * - * The form element must be labeled for accessibility reasons. - * If not provided, an aria-label or aria-labelledby must be provided instead. - */ - label?: React.ReactNode; - /** Provide additional descriptive text for the form element. */ - description?: React.ReactNode; - /** Indicates that the form element is disabled. */ - disabled?: boolean; - /** Indicates that user input is required on the form element. */ - required?: boolean; - /** - * The status of the form element. - * - * Valid is correct, invalid is incorrect and standBy means no validations have run. - * - * When uncontrolled and unspecified it will default to "standBy" and change to either "valid" - * or "invalid" after any change to the state. - */ - status?: HvFormStatus; /** The error message to show when `status` is "invalid". Defaults to "Required". */ statusMessage?: React.ReactNode; /** The callback fired when the cancel button is clicked. */ @@ -126,8 +101,8 @@ export const HvFilterGroup = forwardRef( className, id, name, - required = false, - disabled = false, + required, + disabled, label, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, diff --git a/packages/core/src/FormElement/Adornment/Adornment.tsx b/packages/core/src/FormElement/Adornment/Adornment.tsx index 1dbf0e8e56..0c485d30b9 100644 --- a/packages/core/src/FormElement/Adornment/Adornment.tsx +++ b/packages/core/src/FormElement/Adornment/Adornment.tsx @@ -56,19 +56,17 @@ export const HvAdornment = forwardRef< classes: classesProp, className, icon, - showWhen = undefined, + showWhen, onClick, - isVisible = undefined, + isVisible, ...others } = useDefaultProps("HvAdornment", props); const { classes, cx } = useClasses(classesProp); - const { elementStatus = "", elementDisabled } = - useContext(HvFormElementContext); + const { status, disabled } = useContext(HvFormElementContext); const { input } = useContext(HvFormElementDescriptorsContext); - const displayIcon = - isVisible ?? (showWhen === undefined || elementStatus === showWhen); + const displayIcon = isVisible ?? (showWhen == null || status === showWhen); const isClickable = !!onClick; @@ -86,14 +84,14 @@ export const HvAdornment = forwardRef< classes.adornmentButton, { [classes.hideIcon]: !displayIcon, - [classes.disabled]: elementDisabled, + [classes.disabled]: disabled, }, className, )} onClick={onClick} onMouseDown={(event) => event.preventDefault()} onKeyDown={noop} - disabled={elementDisabled} + disabled={disabled} {...others} >
{icon}
@@ -108,7 +106,7 @@ export const HvAdornment = forwardRef< classes.adornmentIcon, { [classes.hideIcon]: !displayIcon, - [classes.disabled]: elementDisabled, + [classes.disabled]: disabled, }, className, )} diff --git a/packages/core/src/FormElement/CharCounter/CharCounter.tsx b/packages/core/src/FormElement/CharCounter/CharCounter.tsx index c6f528d30c..282f186b16 100644 --- a/packages/core/src/FormElement/CharCounter/CharCounter.tsx +++ b/packages/core/src/FormElement/CharCounter/CharCounter.tsx @@ -42,41 +42,41 @@ export const HvCharCounter = (props: HvCharCounterProps) => { currentCharQuantity = 0, classes: classesProp, className, - id, - disabled, + id: idProp, + disabled: disabledProp, disableGutter = false, ...others } = useDefaultProps("HvCharCounter", props); const { classes, cx } = useClasses(classesProp); - const { elementId, elementDisabled } = useContext(HvFormElementContext); - const localDisabled = disabled || elementDisabled; - const localId = id ?? setId(elementId, "counter"); - const currentId = setId(localId, "currentQuantity"); - const maxQuantityId = setId(localId, "maxQuantity"); + const context = useContext(HvFormElementContext); + const disabled = disabledProp ?? context.disabled; + const id = idProp ?? setId(context.id, "counter"); + const currentId = setId(id, "currentQuantity"); + const maxQuantityId = setId(id, "maxQuantity"); const isOverloaded = currentCharQuantity > maxCharQuantity; return (
{ { - it("should be defined", () => { - const { container } = render(); - expect(container).toBeDefined(); + it("renders the required character", () => { + render( + + + , + ); + + expect(screen.getByText("MY_LABEL")).toBeInTheDocument(); + expect(screen.getByText(/\*/)).toBeInTheDocument(); + }); + + it("renders the error message when status is invalid", () => { + render( + + MY_ERROR_MESSAGE + , + ); + + expect(screen.getByText("MY_ERROR_MESSAGE")).toBeInTheDocument(); }); }); diff --git a/packages/core/src/FormElement/FormElement.tsx b/packages/core/src/FormElement/FormElement.tsx index b0bcc5f75f..e407f83889 100644 --- a/packages/core/src/FormElement/FormElement.tsx +++ b/packages/core/src/FormElement/FormElement.tsx @@ -10,6 +10,7 @@ import { HvFormElementContext, HvFormElementDescriptorsContext, HvFormElementValueContext, + type HvFormElementContextValue, } from "./context"; import { staticClasses, useClasses } from "./FormElement.styles"; import { findDescriptors } from "./utils"; @@ -20,20 +21,15 @@ export type HvFormElementClasses = ExtractNames; export type HvFormStatus = "standBy" | "valid" | "invalid" | "empty"; -export interface HvFormElementProps - extends HvBaseProps { - /** - * Name of the form element. - * - * Part of a name/value pair, should be the name property of the underling native input. - */ - name?: string; +export interface HvFormElementProps + extends HvFormElementContextValue, + HvBaseProps { /** * Current value of the form element. * * Part of a name/value pair, should be the value property of the underling native input. */ - value?: any; + value?: Value; /** * The label of the form element. * @@ -43,21 +39,6 @@ export interface HvFormElementProps label?: React.ReactNode; /** Provide additional descriptive text for the form element. */ description?: React.ReactNode; - /** Whether the form element is disabled. */ - disabled?: boolean; - /** Indicates that the form element is not editable. */ - readOnly?: boolean; - /** Indicates that user input is required on the form element. */ - required?: boolean; - /** - * The status of the form element. - * - * Valid is correct, invalid is incorrect and standBy means no validations have run. - * - * When uncontrolled and unspecified it will default to "standBy" and change to either "valid" - * or "invalid" after any change to the state. - */ - status?: HvFormStatus; /** The error message to show when `status` is "invalid". */ statusMessage?: string; /** The callback fired when the value changes. */ @@ -66,35 +47,34 @@ export interface HvFormElementProps classes?: HvFormElementClasses; } +/** + * Provides form-related context (ie. required/disabled/readOnly) for building form components, + * analogous to MUI's [`FormControl`](https://mui.com/material-ui/api/form-control/) component. + * + * It is used internally to build UI Kit's form components (eg. `HvInput`, `HvDatePicker`), and can be used to build custom form components. + */ export const HvFormElement = (props: HvFormElementProps) => { const { classes: classesProp, className, children, - id, + id: idProp, name, value, - disabled = false, - required = false, - readOnly = false, + disabled, + required, + readOnly, status = "standBy", ...others } = useDefaultProps("HvFormElement", props); const { classes, cx } = useClasses(classesProp); - const elementId = useUniqueId(id); + const id = useUniqueId(idProp); - const contextValue = useMemo( - () => ({ - elementId, - elementName: name, - elementStatus: status, - elementDisabled: disabled, - elementRequired: required, - elementReadOnly: readOnly, - }), - [disabled, elementId, name, readOnly, required, status], + const contextValue = useMemo( + () => ({ id, name, status, disabled, required, readOnly }), + [id, name, status, disabled, required, readOnly], ); const descriptors = useMemo(() => findDescriptors(children), [children]); diff --git a/packages/core/src/FormElement/InfoMessage/InfoMessage.tsx b/packages/core/src/FormElement/InfoMessage/InfoMessage.tsx index 44fa865e04..189decb91e 100644 --- a/packages/core/src/FormElement/InfoMessage/InfoMessage.tsx +++ b/packages/core/src/FormElement/InfoMessage/InfoMessage.tsx @@ -38,9 +38,9 @@ export const HvInfoMessage = (props: HvInfoMessageProps) => { const { classes, cx } = useClasses(classesProp); - const { elementId, elementDisabled } = useContext(HvFormElementContext); - const disabled = disabledProp ?? elementDisabled; - const id = idProp ?? setId(elementId, "description"); + const context = useContext(HvFormElementContext); + const disabled = disabledProp ?? context.disabled; + const id = idProp ?? setId(context.id, "description"); return ( { */ export const HvLabel = (props: HvLabelProps) => { const { - id, + id: idProp, classes: classesProp, className, children, label, - disabled, - required, + disabled: disabledProp, + required: requiredProp, htmlFor: htmlForProp, ...others } = useDefaultProps("HvLabel", props); const { classes, cx } = useClasses(classesProp); - const { elementId, elementDisabled, elementRequired } = - useContext(HvFormElementContext); + const context = useContext(HvFormElementContext); - const localDisabled = disabled || elementDisabled; - const localRequired = required || elementRequired; - - const localId = id ?? setId(elementId, "label"); + const disabled = disabledProp ?? context.disabled; + const required = requiredProp ?? context.required; + const id = idProp ?? setId(context.id, "label"); const forId = htmlForProp || findDescriptors(children)?.input?.[0]?.id; return ( <> { {...others} > {label} - {localRequired && } + {required && } {children} diff --git a/packages/core/src/FormElement/Suggestions/Suggestions.tsx b/packages/core/src/FormElement/Suggestions/Suggestions.tsx index 0af0fb8d2a..3b20738b4d 100644 --- a/packages/core/src/FormElement/Suggestions/Suggestions.tsx +++ b/packages/core/src/FormElement/Suggestions/Suggestions.tsx @@ -78,8 +78,8 @@ export const HvSuggestions = forwardRef< const { rootId } = useTheme(); - const { elementId } = useContext(HvFormElementContext); - const id = idProp ?? setId(elementId, "suggestions"); + const context = useContext(HvFormElementContext); + const id = idProp ?? setId(context.id, "suggestions"); const ref = useRef(null); const forkedRef = useForkRef(ref, extRef); diff --git a/packages/core/src/FormElement/WarningText/WarningText.tsx b/packages/core/src/FormElement/WarningText/WarningText.tsx index 3faf3ac7ac..b47fa9d22d 100644 --- a/packages/core/src/FormElement/WarningText/WarningText.tsx +++ b/packages/core/src/FormElement/WarningText/WarningText.tsx @@ -54,11 +54,10 @@ export const HvWarningText = (props: HvWarningTextProps) => { const { classes, cx } = useClasses(classesProp); - const { elementId, elementStatus, elementDisabled } = - useContext(HvFormElementContext); - const disabled = disabledProp || elementDisabled; - const visible = isVisibleProp ?? elementStatus === "invalid"; - const id = idProp ?? setId(elementId, "error"); + const context = useContext(HvFormElementContext); + const disabled = disabledProp ?? context.disabled; + const visible = isVisibleProp ?? context.status === "invalid"; + const id = idProp ?? setId(context.id, "error"); const showWarning = visible && !disabled; const adornment = adornmentProp || ( diff --git a/packages/core/src/FormElement/context.ts b/packages/core/src/FormElement/context.ts index 751710de17..bc88a2e9ee 100644 --- a/packages/core/src/FormElement/context.ts +++ b/packages/core/src/FormElement/context.ts @@ -1,12 +1,31 @@ import { createContext } from "react"; +import type { HvFormStatus } from "./FormElement"; + export interface HvFormElementContextValue { - elementId?: string; - elementDisabled?: boolean; - elementRequired?: boolean; - elementStatus?: string; - elementReadOnly?: boolean; - elementName?: string; + /** id to be applied to the form element root node. */ + id?: string; + /** + * Name of the form element. + * + * Part of a name/value pair, should be the name property of the underling native input. + */ + name?: string; + /** + * The status of the form element. + * + * Valid is correct, invalid is incorrect and standBy means no validations have run. + * + * When uncontrolled and unspecified it will default to "standBy" and change to either "valid" + * or "invalid" after any change to the state. + */ + status?: HvFormStatus; + /** Whether the form element is disabled. */ + disabled?: boolean; + /** Indicates that user input is required on the form element. */ + required?: boolean; + /** Indicates that the form element is not editable. */ + readOnly?: boolean; } export const HvFormElementContext = createContext( diff --git a/packages/core/src/FormElement/utils.ts b/packages/core/src/FormElement/utils.ts index a4a902b8a2..a733b968bf 100644 --- a/packages/core/src/FormElement/utils.ts +++ b/packages/core/src/FormElement/utils.ts @@ -92,11 +92,11 @@ export const buildFormElementPropsFromContext = ( context?: HvFormElementContextValue, ) => { return { - name: name || context?.elementName, - disabled: disabled !== undefined ? disabled : context?.elementDisabled, - readOnly: readOnly !== undefined ? readOnly : context?.elementReadOnly, - required: required !== undefined ? required : context?.elementRequired, - status: context?.elementStatus, + name: name || context?.name, + disabled: disabled ?? context?.disabled, + readOnly: readOnly ?? context?.readOnly, + required: required ?? context?.required, + status: context?.status, }; }; diff --git a/packages/core/src/Input/Input.tsx b/packages/core/src/Input/Input.tsx index 8734c8af89..1a067c14ef 100644 --- a/packages/core/src/Input/Input.tsx +++ b/packages/core/src/Input/Input.tsx @@ -55,7 +55,6 @@ import { useIsMounted } from "../hooks/useIsMounted"; import { useLabels } from "../hooks/useLabels"; import { useUniqueId } from "../hooks/useUniqueId"; import { HvTooltip } from "../Tooltip"; -import { HvInputSuggestion, HvValidationMessages } from "../types/forms"; import { isKey } from "../utils/keyboardUtils"; import { setId } from "../utils/setId"; import { staticClasses, useClasses } from "./Input.styles"; @@ -66,6 +65,25 @@ export type HvInputClasses = ExtractNames; type InputElement = HTMLInputElement | HTMLTextAreaElement; +export interface HvValidationMessages { + /** The value when a validation fails. */ + error?: string; + /** The message that appears when there are too many characters. */ + maxCharError?: string; + /** The message that appears when there are too few characters. */ + minCharError?: string; + /** The message that appears when the input is empty and required. */ + requiredError?: string; + /** The message that appears when the input is value is incompatible with the expected type. */ + typeMismatchError?: string; +} + +export interface HvInputSuggestion { + id: string; + label: string; + value?: string; +} + export interface HvInputProps extends Omit { /** The form element name. */ @@ -241,9 +259,9 @@ export const HvInput = forwardRef< name, value: valueProp, defaultValue = "", - required = false, - readOnly = false, - disabled = false, + required, + readOnly, + disabled, enablePortal = false, suggestOnFocus = false, label, diff --git a/packages/core/src/Radio/Radio.tsx b/packages/core/src/Radio/Radio.tsx index 361d4261d2..4810f79d20 100644 --- a/packages/core/src/Radio/Radio.tsx +++ b/packages/core/src/Radio/Radio.tsx @@ -139,9 +139,9 @@ export const HvRadio = forwardRef( id, name, value = "on", - required = false, - readOnly = false, - disabled = false, + required, + readOnly, + disabled, label, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, @@ -153,7 +153,7 @@ export const HvRadio = forwardRef( status = "standBy", statusMessage, "aria-errormessage": ariaErrorMessage, - semantic = false, + semantic, inputProps, onFocusVisible, onBlur, @@ -182,10 +182,7 @@ export const HvRadio = forwardRef( [onBlur], ); - const [isChecked, setIsChecked] = useControlled( - checked, - Boolean(defaultChecked), - ); + const [isChecked, setIsChecked] = useControlled(checked, defaultChecked); const onLocalChange = useCallback( (evt: React.ChangeEvent, newChecked: boolean) => { diff --git a/packages/core/src/RadioGroup/RadioGroup.tsx b/packages/core/src/RadioGroup/RadioGroup.tsx index 848cbb8e3f..0831b829ac 100644 --- a/packages/core/src/RadioGroup/RadioGroup.tsx +++ b/packages/core/src/RadioGroup/RadioGroup.tsx @@ -138,9 +138,9 @@ export const HvRadioGroup = forwardRef( description, status, statusMessage, - required = false, - readOnly = false, - disabled = false, + required, + readOnly, + disabled, orientation = "vertical", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, diff --git a/packages/core/src/SelectionList/SelectionList.tsx b/packages/core/src/SelectionList/SelectionList.tsx index 613a855fb6..72a6a662bb 100644 --- a/packages/core/src/SelectionList/SelectionList.tsx +++ b/packages/core/src/SelectionList/SelectionList.tsx @@ -127,9 +127,9 @@ export const HvSelectionList = forwardRef< name, value: valueProp, defaultValue, - required = false, - readOnly = false, - disabled = false, + required, + readOnly, + disabled, label, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, diff --git a/packages/core/src/Slider/Slider.tsx b/packages/core/src/Slider/Slider.tsx index 87ace52adf..63694d71a5 100644 --- a/packages/core/src/Slider/Slider.tsx +++ b/packages/core/src/Slider/Slider.tsx @@ -161,9 +161,9 @@ export const HvSlider = forwardRef((props, ref) => { requiredMessage = "The value is required", outOfRangeMessage = "The value is out of range", noOverlap = true, - hideInput = false, - required = false, - readOnly = false, + hideInput, + required, + readOnly, markProperties = [], defaultValues = [undefined], values: valuesProp = [], diff --git a/packages/core/src/Switch/Switch.tsx b/packages/core/src/Switch/Switch.tsx index 5e707252f4..178b86fb27 100644 --- a/packages/core/src/Switch/Switch.tsx +++ b/packages/core/src/Switch/Switch.tsx @@ -135,9 +135,9 @@ export const HvSwitch = forwardRef( id, name, value = "on", - required = false, - readOnly = false, - disabled = false, + required, + readOnly, + disabled, label, "aria-label": ariaLabel, @@ -165,10 +165,7 @@ export const HvSwitch = forwardRef( const elementId = useUniqueId(id); - const [isChecked, setIsChecked] = useControlled( - checked, - Boolean(defaultChecked), - ); + const [isChecked, setIsChecked] = useControlled(checked, defaultChecked); const [validationState, setValidationState] = useControlled( status, diff --git a/packages/core/src/TagsInput/TagsInput.tsx b/packages/core/src/TagsInput/TagsInput.tsx index 6e73ba289c..0ecd7219a3 100644 --- a/packages/core/src/TagsInput/TagsInput.tsx +++ b/packages/core/src/TagsInput/TagsInput.tsx @@ -33,16 +33,17 @@ import { import { useControlled } from "../hooks/useControlled"; import { useIsMounted } from "../hooks/useIsMounted"; import { useUniqueId } from "../hooks/useUniqueId"; -import { HvInput, HvInputProps } from "../Input"; +import { HvInput, HvInputProps, HvInputSuggestion } from "../Input"; import { HvListContainer, HvListItem } from "../ListContainer"; import { HvTag, HvTagProps } from "../Tag"; -import { HvTagSuggestion } from "../types/forms"; import { isKey } from "../utils/keyboardUtils"; import { setId } from "../utils/setId"; import { staticClasses, useClasses } from "./TagsInput.styles"; export { staticClasses as tagsInputClasses }; +export interface HvTagSuggestion extends HvInputSuggestion {} + export type HvTagsInputClasses = ExtractNames; export interface HvTagsInputProps @@ -124,9 +125,9 @@ export const HvTagsInput = forwardRef( name, value: valueProp, defaultValue = [], - readOnly = false, - disabled = false, - required = false, + readOnly, + disabled, + required, label: textAreaLabel, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, @@ -138,21 +139,21 @@ export const HvTagsInput = forwardRef( onBlur, onFocus, placeholder, - hideCounter = false, + hideCounter, middleCountLabel = "/", maxTagsQuantity, resizable = true, inputProps, countCharProps, - multiline = false, + multiline, status, statusMessage, validationMessages, commitTagOn = ["Enter"], - commitOnBlur = false, + commitOnBlur, suggestionListCallback, suggestionValidation, - suggestionsLoose = false, + suggestionsLoose, ...others } = useDefaultProps("HvTagsInput", props); diff --git a/packages/core/src/TextArea/TextArea.stories.tsx b/packages/core/src/TextArea/TextArea.stories.tsx index e08941f71e..320d618355 100644 --- a/packages/core/src/TextArea/TextArea.stories.tsx +++ b/packages/core/src/TextArea/TextArea.stories.tsx @@ -27,159 +27,8 @@ export const Main: StoryObj = { statusMessage: "Oops, something's gone wrong!", }, argTypes: { - label: { - description: - "The label of the form element. The form element must be labeled for accessibility reasons. If not provided, an aria-label or aria-labelledby must be provided instead.", - table: { - type: { summary: "React.ReactNode" }, - }, - }, - placeholder: { - description: "The placeholder value of the text area.", - table: { - type: { summary: "string" }, - }, - }, - description: { - description: "Provide additional descriptive text for the form element.", - table: { - type: { summary: "React.ReactNode" }, - }, - }, - status: { - description: - "The status of the form element. Valid is correct, invalid is incorrect and standBy means no validations have run. When uncontrolled and unspecified it will default to `standBy` and change to either `valid` or `invalid` after any change to the state.", - table: { - type: { summary: "HvFormStatus" }, - }, - }, - statusMessage: { - description: "The error message to show when `status` is `invalid`.", - table: { - type: { summary: "React.ReactNode" }, - }, - }, - middleCountLabel: { - description: "Text between the current char counter and max value.", - table: { - defaultValue: { summary: "/" }, - type: { summary: "string" }, - }, - }, - validationMessages: { - description: - "An Object containing the various texts associated with the input.", - table: { - type: { summary: "HvValidationMessages" }, - }, - }, - validation: { - description: - "The custom validation function, it receives the value and must return either `true` for valid or `false` for invalid, default validations would only occur if this function is null or undefined.", - table: { - type: { summary: "(value: string) => boolean" }, - }, - }, - maxCharQuantity: { - description: - "The maximum allowed length of the characters, if this value is null no check will be performed.", - table: { - type: { summary: "number" }, - }, - }, - minCharQuantity: { - description: - "The minimum allowed length of the characters, if this value is null no check will be performed.", - table: { - type: { summary: "number" }, - }, - }, - autoFocus: { - description: "If `true` it should autofocus.", - table: { - defaultValue: { summary: "false" }, - type: { summary: "boolean" }, - }, - }, - rows: { - description: "The number of rows of the text area.", - table: { - type: { summary: "number" }, - }, - }, - resizable: { - description: "If `true` the component is resizable.", - table: { - defaultValue: { summary: "false" }, - type: { summary: "boolean" }, - }, - }, - autoScroll: { - description: - "Auto-scroll: automatically scroll to the end on value changes. Will stop if the user scrolls up and resume if scrolled to the bottom.", - table: { - defaultValue: { summary: "false" }, - type: { summary: "boolean" }, - }, - }, - blockMax: { - description: "If `true` it isn't possible to pass the `maxCharQuantity`.", - table: { - defaultValue: { summary: "false" }, - type: { summary: "boolean" }, - }, - }, - hideCounter: { - description: - "If `true` the character counter isn't shown even if `maxCharQuantity` is set.", - table: { - defaultValue: { summary: "false" }, - type: { summary: "boolean" }, - }, - }, - countCharProps: { - description: "Props passed to the char count.", - control: { disable: true }, - table: { - type: { summary: "Partial" }, - }, - }, - onChange: { - description: "Called back when the value is changed.", - table: { - type: { - summary: - "(event: React.ChangeEvent, value: string) => void", - }, - }, - }, - onBlur: { - description: "Called back when the value is changed.", - table: { - type: { - summary: - "(event: React.FocusEvent, value: string, validationState: HvInputValidity) => void", - }, - }, - }, - onFocus: { - description: - "The function that will be executed onBlur, allows checking the value state, it receives the value.", - table: { - type: { - summary: - "(event: React.FocusEvent, value: string) => void", - }, - }, - }, - classes: { - description: - "A Jss Object used to override or extend the styles applied to the component.", - table: { - type: { summary: "HvTextAreaClasses" }, - }, - control: { disable: true }, - }, + classes: { control: { disable: true } }, + countCharProps: { control: { disable: true } }, }, render: (args) => { return ; diff --git a/packages/core/src/TextArea/TextArea.tsx b/packages/core/src/TextArea/TextArea.tsx index 0b3c99054b..7ccad31d81 100644 --- a/packages/core/src/TextArea/TextArea.tsx +++ b/packages/core/src/TextArea/TextArea.tsx @@ -36,7 +36,7 @@ import { } from "../FormElement"; import { useControlled } from "../hooks/useControlled"; import { useUniqueId } from "../hooks/useUniqueId"; -import { HvValidationMessages } from "../types/forms"; +import type { HvValidationMessages } from "../Input"; import { setId } from "../utils/setId"; import { staticClasses, useClasses } from "./TextArea.styles"; @@ -184,14 +184,14 @@ export const HvTextArea = forwardRef< middleCountLabel = "/", countCharProps = {}, inputProps = {}, - required = false, - readOnly = false, - disabled = false, - autoFocus = false, - resizable = false, - autoScroll = false, - hideCounter = false, - blockMax = false, + required, + readOnly, + disabled, + autoFocus, + resizable, + autoScroll, + hideCounter, + blockMax, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, diff --git a/packages/core/src/TimePicker/TimePicker.tsx b/packages/core/src/TimePicker/TimePicker.tsx index 6353773b77..1809fc68ac 100644 --- a/packages/core/src/TimePicker/TimePicker.tsx +++ b/packages/core/src/TimePicker/TimePicker.tsx @@ -120,9 +120,9 @@ export const HvTimePicker = forwardRef( id: idProp, name, - required = false, - disabled = false, - readOnly = false, + required, + disabled, + readOnly, label, "aria-label": ariaLabel, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 847bfebe49..388635dfcf 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -140,7 +140,6 @@ export * from "./hooks/useWidth"; export * from "./providers/Provider"; export * from "./providers/ThemeProvider"; -export * from "./types/forms"; export * from "./types/generic"; export * from "./types/theme"; export * from "./types/tokens"; diff --git a/packages/core/src/types/forms.ts b/packages/core/src/types/forms.ts deleted file mode 100644 index 5a2467355f..0000000000 --- a/packages/core/src/types/forms.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface HvValidationMessages { - /** The value when a validation fails. */ - error?: string; - /** The message that appears when there are too many characters. */ - maxCharError?: string; - /** The message that appears when there are too few characters. */ - minCharError?: string; - /** The message that appears when the input is empty and required. */ - requiredError?: string; - /** The message that appears when the input is value is incompatible with the expected type. */ - typeMismatchError?: string; -} - -export interface HvInputSuggestion { - id: string; - label: string; - value?: string; -} - -export interface HvTagSuggestion extends HvInputSuggestion {}