From 8a72d3a50d09a4d9cb8b84f65325975cc80071a4 Mon Sep 17 00:00:00 2001 From: Michele Masciave Date: Fri, 11 Apr 2025 08:40:14 +0200 Subject: [PATCH 01/18] export typeahead utils --- src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/index.ts b/src/index.ts index 1aed903..434f057 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,5 +8,9 @@ export * from "./lib/types/LabelValueOption"; export * from "./lib/DatePickerInput"; export * from "./lib/ColorPickerInput"; export * from "./lib/helpers/dateUtils"; +export * from "./lib/helpers/mui"; +export * from "./lib/helpers/typeahead"; +export * from "./lib/hooks/useDebounceHook"; +export * from "./lib/hooks/useSafeNameId"; export { useFormContext } from "./lib/context/FormContext"; From f8dc2b0a903de315cb0b7d6e9032d320b15674a3 Mon Sep 17 00:00:00 2001 From: Michele Masciave Date: Wed, 16 Apr 2025 12:38:57 +0200 Subject: [PATCH 02/18] integrate placeholder fix and changelog --- CHANGELOG.md | 8 ++++++++ .../component/Typeahead/AsyncTypeaheadInput.cy.tsx | 6 +++--- .../component/Typeahead/StaticTypeaheadInput.cy.tsx | 9 +++------ src/index.ts | 1 - src/lib/AsyncTypeaheadInput.tsx | 2 +- src/lib/StaticTypeaheadInput.tsx | 2 +- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4503f74..c71d00b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Export typeahead helpers and `useDebounceHook` hook. + +### Fixed + +- Hide placeholder on multiple `AsyncTypeAheadInput` and `StaticTypeAheadInput` when at least one option is selected. + ## [3.1.1] - 2025-04-10 ### Fixed diff --git a/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx b/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx index 99dbe00..05f8998 100644 --- a/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx +++ b/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx @@ -641,9 +641,6 @@ it("placeholder", () => { onSubmit={() => { // Nothing to do }} - defaultValues={{ - [name]: simpleOptions, - }} > { , ); + cy.get(`#${name}`).should("have.attr", "placeholder", placeholder); + simpleOptions.slice(0, 2).forEach((option) => selectOption(name, option)); + cy.get(`#${name}`).should("not.have.attr", "placeholder"); }); it("test on input change", () => { diff --git a/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx b/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx index 8ec1066..ad64378 100644 --- a/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx +++ b/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx @@ -398,12 +398,7 @@ it("placeholder", () => { cy.mount(
-
+ @@ -411,6 +406,8 @@ it("placeholder", () => { ); cy.get(`#${name}`).should("have.attr", "placeholder", placeholder); + simpleOptions.slice(0, 2).forEach((option) => selectOption(name, option)); + cy.get(`#${name}`).should("not.have.attr", "placeholder"); }); it("test on input change", () => { diff --git a/src/index.ts b/src/index.ts index 434f057..8831d0a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,5 @@ export * from "./lib/helpers/dateUtils"; export * from "./lib/helpers/mui"; export * from "./lib/helpers/typeahead"; export * from "./lib/hooks/useDebounceHook"; -export * from "./lib/hooks/useSafeNameId"; export { useFormContext } from "./lib/context/FormContext"; diff --git a/src/lib/AsyncTypeaheadInput.tsx b/src/lib/AsyncTypeaheadInput.tsx index 42f8662..c2892a2 100644 --- a/src/lib/AsyncTypeaheadInput.tsx +++ b/src/lib/AsyncTypeaheadInput.tsx @@ -206,7 +206,7 @@ const AsyncTypeaheadInput = (props: AsyncTypeaheadInputPr hideValidationMessage={hideValidationMessage} useBootstrapStyle={useBootstrapStyle} helpText={helpText} - placeholder={placeholder} + placeholder={multiple && value.length > 0 ? undefined : placeholder} paginationIcon={paginationIcon} paginationText={paginationText} variant={variant} diff --git a/src/lib/StaticTypeaheadInput.tsx b/src/lib/StaticTypeaheadInput.tsx index b72e47c..ad59824 100644 --- a/src/lib/StaticTypeaheadInput.tsx +++ b/src/lib/StaticTypeaheadInput.tsx @@ -169,7 +169,7 @@ const StaticTypeaheadInput = (props: StaticTypeaheadInput hideValidationMessage={hideValidationMessage} useBootstrapStyle={useBootstrapStyle} helpText={helpText} - placeholder={placeholder} + placeholder={multiple && value.length > 0 ? undefined : placeholder} paginationIcon={paginationIcon} paginationText={paginationText} variant={variant} From a8573843aaf2a7d2b9f1de219c05ce930b328464 Mon Sep 17 00:00:00 2001 From: Michele Masciave Date: Wed, 16 Apr 2025 15:27:23 +0200 Subject: [PATCH 03/18] adjust typescript --- src/lib/AsyncTypeaheadInput.tsx | 10 ++++++---- src/lib/StaticTypeaheadInput.tsx | 4 +++- src/lib/helpers/typeahead.tsx | 20 ++++++++++---------- src/lib/hooks/useDebounceHook.ts | 4 ++-- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/lib/AsyncTypeaheadInput.tsx b/src/lib/AsyncTypeaheadInput.tsx index c2892a2..cab92da 100644 --- a/src/lib/AsyncTypeaheadInput.tsx +++ b/src/lib/AsyncTypeaheadInput.tsx @@ -69,8 +69,8 @@ const AsyncTypeaheadInput = (props: AsyncTypeaheadInputPr autocompleteProps, } = props; - const [options, setOptions] = useState(defaultOptions); - const [value, setValue] = useState(defaultSelected); + const [options, setOptions] = useState(defaultOptions); + const [value, setValue] = useState(defaultSelected); const [page, setPage] = useState(1); const [loadMoreOptions, setLoadMoreOptions] = useState(limitResults !== undefined && limitResults < defaultOptions.length); const { name, id } = useSafeNameId(props.name ?? "", props.id); @@ -107,7 +107,7 @@ const AsyncTypeaheadInput = (props: AsyncTypeaheadInputPr defaultSelected.map((x) => (typeof x === "string" ? x : x.value)), ); }, - updateValues: (options: TypeaheadOption[]) => { + updateValues: (options: TypeaheadOptions) => { const values = convertAutoCompleteOptionsToStringArray(options); const finalValue = multiple ? values : values[0]; setValue(options); @@ -171,7 +171,9 @@ const AsyncTypeaheadInput = (props: AsyncTypeaheadInputPr field.onBlur(); }} onChange={(_e, value) => { - const optionsArray = value ? (Array.isArray(value) ? value : [value]) : undefined; + // value is typed as Autocomplete (aka TypeaheadOption) or an array of Autocomplete (aka TypeaheadOption[]) + // however, the component is not intended to be used with mixed types + const optionsArray = value ? (Array.isArray(value) ? value : [value]) as TypeaheadOptions : undefined; setValue(optionsArray ?? []); const values = convertAutoCompleteOptionsToStringArray(optionsArray); const finalValue = multiple ? values : values[0]; diff --git a/src/lib/StaticTypeaheadInput.tsx b/src/lib/StaticTypeaheadInput.tsx index ad59824..715281a 100644 --- a/src/lib/StaticTypeaheadInput.tsx +++ b/src/lib/StaticTypeaheadInput.tsx @@ -140,7 +140,9 @@ const StaticTypeaheadInput = (props: StaticTypeaheadInput field.onBlur(); }} onChange={(_, value) => { - const optionsArray = value ? (Array.isArray(value) ? value : [value]) : undefined; + // value is typed as Autocomplete (aka TypeaheadOption) or an array of Autocomplete (aka TypeaheadOption[]) + // however, the component is not intended to be used with mixed types + const optionsArray = value ? (Array.isArray(value) ? value : [value]) as TypeaheadOptions : undefined; const values = convertAutoCompleteOptionsToStringArray(optionsArray); const finalValue = multiple ? values : values[0]; clearErrors(field.name); diff --git a/src/lib/helpers/typeahead.tsx b/src/lib/helpers/typeahead.tsx index 7aef6be..de85e7a 100644 --- a/src/lib/helpers/typeahead.tsx +++ b/src/lib/helpers/typeahead.tsx @@ -1,12 +1,12 @@ import { AutocompleteRenderOptionState } from "@mui/material/Autocomplete"; import { LabelValueOption } from "../types/LabelValueOption"; -import { TypeaheadOption } from "../types/Typeahead"; +import { TypeaheadOption, TypeaheadOptions } from "../types/Typeahead"; import AutosuggestHighlightMatch from "autosuggest-highlight/match"; import AutosuggestHighlightParse from "autosuggest-highlight/parse"; -const isStringArray = (options: TypeaheadOption[]): boolean => options.length > 0 && options.every((value) => typeof value === "string"); +const isStringArray = (options: TypeaheadOptions): boolean => options.length > 0 && (options as TypeaheadOption[]).every((value) => typeof value === "string"); -const convertAutoCompleteOptionsToStringArray = (options: TypeaheadOption[] | undefined): string[] => { +const convertAutoCompleteOptionsToStringArray = (options: TypeaheadOptions | undefined): string[] => { if (!options) { return []; } @@ -18,29 +18,29 @@ const convertAutoCompleteOptionsToStringArray = (options: TypeaheadOption[] | un return (options as LabelValueOption[]).map((option) => option.value) as string[]; }; -const getSingleAutoCompleteValue = (options: TypeaheadOption[], fieldValue: string | number | undefined): TypeaheadOption[] => { +const getSingleAutoCompleteValue = (options: TypeaheadOptions, fieldValue: string | number | undefined): TypeaheadOptions => { if (fieldValue == undefined) { return []; } - return options.filter((x) => + return (options as TypeaheadOption[]).filter((x) => // loose equality check to handle different types between form value and option value typeof x === "string" ? x == fieldValue : x.value == fieldValue, - ); + ) as TypeaheadOptions; }; -const getMultipleAutoCompleteValue = (options: TypeaheadOption[], fieldValue: (string | number)[] | undefined): TypeaheadOption[] => { +const getMultipleAutoCompleteValue = (options: TypeaheadOptions, fieldValue: (string | number)[] | undefined): TypeaheadOptions => { if (fieldValue == undefined) { return []; } - return options.filter((x) => + return (options as TypeaheadOption[]).filter((x: TypeaheadOption) => typeof x === "string" ? fieldValue.includes(x) : // ensure that form values matches options values even if they are of different types fieldValue.map(String).includes(String(x.value as string | number)), - ); + ) as TypeaheadOptions; }; -const sortOptionsByGroup = (options: TypeaheadOption[]): TypeaheadOption[] => +const sortOptionsByGroup = (options: TypeaheadOptions): TypeaheadOptions => options.sort((x, y) => (typeof x === "string" ? x : x.group?.name ?? "").localeCompare(typeof y === "string" ? y : y.group?.name ?? "")); const groupOptions = (option: TypeaheadOption): string => (typeof option === "string" ? option : option.group?.name ?? ""); diff --git a/src/lib/hooks/useDebounceHook.ts b/src/lib/hooks/useDebounceHook.ts index c97f72c..31d4595 100644 --- a/src/lib/hooks/useDebounceHook.ts +++ b/src/lib/hooks/useDebounceHook.ts @@ -1,12 +1,12 @@ import { useEffect, useRef, useState } from "react"; -import { TypeaheadOption, TypeaheadOptions } from "../types/Typeahead"; +import { TypeaheadOptions } from "../types/Typeahead"; interface DebounceSearch { query: string; delay: number; } -const useDebounceHook = (queryFn: (query: string) => Promise, setOptions: (results: TypeaheadOption[]) => void) => { +const useDebounceHook = (queryFn: (query: string) => Promise, setOptions: (results: TypeaheadOptions) => void) => { const queryRef = useRef(""); const [isLoading, setIsLoading] = useState(false); const [debounceSearch, setDebounceSearch] = useState(undefined); From e1267ef282dd50afce477920d143f4a4ee555a8c Mon Sep 17 00:00:00 2001 From: Michele Masciave Date: Mon, 28 Apr 2025 09:34:08 +0200 Subject: [PATCH 04/18] prepare-pr --- CHANGELOG.md | 1 + src/lib/AsyncTypeaheadInput.tsx | 2 +- src/lib/StaticTypeaheadInput.tsx | 2 +- src/lib/helpers/typeahead.tsx | 5 +++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c71d00b..dc11842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Export typeahead helpers and `useDebounceHook` hook. +- Typeahead helpers return type from `TypeaheadOption[]` to `TypeaheadOptions`, since typeaheads are not intended to be used with mixed `string` and `LabelValueOption` options. ### Fixed diff --git a/src/lib/AsyncTypeaheadInput.tsx b/src/lib/AsyncTypeaheadInput.tsx index cab92da..c2850f3 100644 --- a/src/lib/AsyncTypeaheadInput.tsx +++ b/src/lib/AsyncTypeaheadInput.tsx @@ -173,7 +173,7 @@ const AsyncTypeaheadInput = (props: AsyncTypeaheadInputPr onChange={(_e, value) => { // value is typed as Autocomplete (aka TypeaheadOption) or an array of Autocomplete (aka TypeaheadOption[]) // however, the component is not intended to be used with mixed types - const optionsArray = value ? (Array.isArray(value) ? value : [value]) as TypeaheadOptions : undefined; + const optionsArray = value ? ((Array.isArray(value) ? value : [value]) as TypeaheadOptions) : undefined; setValue(optionsArray ?? []); const values = convertAutoCompleteOptionsToStringArray(optionsArray); const finalValue = multiple ? values : values[0]; diff --git a/src/lib/StaticTypeaheadInput.tsx b/src/lib/StaticTypeaheadInput.tsx index 715281a..9e29134 100644 --- a/src/lib/StaticTypeaheadInput.tsx +++ b/src/lib/StaticTypeaheadInput.tsx @@ -142,7 +142,7 @@ const StaticTypeaheadInput = (props: StaticTypeaheadInput onChange={(_, value) => { // value is typed as Autocomplete (aka TypeaheadOption) or an array of Autocomplete (aka TypeaheadOption[]) // however, the component is not intended to be used with mixed types - const optionsArray = value ? (Array.isArray(value) ? value : [value]) as TypeaheadOptions : undefined; + const optionsArray = value ? ((Array.isArray(value) ? value : [value]) as TypeaheadOptions) : undefined; const values = convertAutoCompleteOptionsToStringArray(optionsArray); const finalValue = multiple ? values : values[0]; clearErrors(field.name); diff --git a/src/lib/helpers/typeahead.tsx b/src/lib/helpers/typeahead.tsx index de85e7a..d552b32 100644 --- a/src/lib/helpers/typeahead.tsx +++ b/src/lib/helpers/typeahead.tsx @@ -4,7 +4,8 @@ import { TypeaheadOption, TypeaheadOptions } from "../types/Typeahead"; import AutosuggestHighlightMatch from "autosuggest-highlight/match"; import AutosuggestHighlightParse from "autosuggest-highlight/parse"; -const isStringArray = (options: TypeaheadOptions): boolean => options.length > 0 && (options as TypeaheadOption[]).every((value) => typeof value === "string"); +const isStringArray = (options: TypeaheadOptions): boolean => + options.length > 0 && (options as TypeaheadOption[]).every((value) => typeof value === "string"); const convertAutoCompleteOptionsToStringArray = (options: TypeaheadOptions | undefined): string[] => { if (!options) { @@ -32,7 +33,7 @@ const getMultipleAutoCompleteValue = (options: TypeaheadOptions, fieldValue: (st if (fieldValue == undefined) { return []; } - return (options as TypeaheadOption[]).filter((x: TypeaheadOption) => + return (options as TypeaheadOption[]).filter((x) => typeof x === "string" ? fieldValue.includes(x) : // ensure that form values matches options values even if they are of different types From 12211f1448a0b73a6d7b8e9795a8e7633706d4ae Mon Sep 17 00:00:00 2001 From: Michele Masciave Date: Wed, 30 Apr 2025 11:41:20 +0200 Subject: [PATCH 05/18] fixed --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc11842..99f5cb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Export typeahead helpers and `useDebounceHook` hook. -- Typeahead helpers return type from `TypeaheadOption[]` to `TypeaheadOptions`, since typeaheads are not intended to be used with mixed `string` and `LabelValueOption` options. ### Fixed - Hide placeholder on multiple `AsyncTypeAheadInput` and `StaticTypeAheadInput` when at least one option is selected. +- Typeahead helpers return type from `TypeaheadOption[]` to `TypeaheadOptions`, since typeaheads are not intended to be used with mixed `string` and `LabelValueOption` options. ## [3.1.1] - 2025-04-10 From 9623ce9d0ef191ac6fa3af97ae5a52918262843a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michele=20Masciav=C3=A8?= Date: Wed, 15 Apr 2026 15:25:43 +0200 Subject: [PATCH 06/18] x --- CHANGELOG.md | 4 ++++ src/lib/components/ColorPicker/ColorPicker.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1bcbcf..2cda7cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- `ColorPicker` controlled/uncontrolled state warning by ensuring value prop is always defined. + ## [4.1.0] - 2026-03-16 ### Added diff --git a/src/lib/components/ColorPicker/ColorPicker.tsx b/src/lib/components/ColorPicker/ColorPicker.tsx index 4bea795..6f743ce 100644 --- a/src/lib/components/ColorPicker/ColorPicker.tsx +++ b/src/lib/components/ColorPicker/ColorPicker.tsx @@ -107,7 +107,7 @@ const ColorPicker = (props: ColorPickerInputProps) => field.onBlur(); }} - value={field.value} + value={field.value ?? null} slotProps={{ input: { startAdornment: ( From 57413ecadb10ef0c27b057995d8bf4052ada346f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michele=20Masciav=C3=A8?= Date: Wed, 15 Apr 2026 15:53:14 +0200 Subject: [PATCH 07/18] x --- src/lib/components/ColorPicker/ColorPicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/ColorPicker/ColorPicker.tsx b/src/lib/components/ColorPicker/ColorPicker.tsx index 6f743ce..fcf8369 100644 --- a/src/lib/components/ColorPicker/ColorPicker.tsx +++ b/src/lib/components/ColorPicker/ColorPicker.tsx @@ -107,7 +107,7 @@ const ColorPicker = (props: ColorPickerInputProps) => field.onBlur(); }} - value={field.value ?? null} + value={field.value || ""} slotProps={{ input: { startAdornment: ( From e318bbe939508913e7e5fcf41fde1ea6de90e567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michele=20Masciav=C3=A8?= Date: Wed, 15 Apr 2026 16:15:20 +0200 Subject: [PATCH 08/18] fix --- CHANGELOG.md | 1 + src/lib/components/ColorPicker/ColorPicker.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cda7cb..4b2b9df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - `ColorPicker` controlled/uncontrolled state warning by ensuring value prop is always defined. +- `ColorPicker` reset value when `convertColorToFormatOrUndefinedOnBlur` is enabled. ## [4.1.0] - 2026-03-16 diff --git a/src/lib/components/ColorPicker/ColorPicker.tsx b/src/lib/components/ColorPicker/ColorPicker.tsx index fcf8369..30e2c5a 100644 --- a/src/lib/components/ColorPicker/ColorPicker.tsx +++ b/src/lib/components/ColorPicker/ColorPicker.tsx @@ -98,6 +98,7 @@ const ColorPicker = (props: ColorPickerInputProps) => }} onBlur={(e) => { if (convertColorToFormatOrUndefinedOnBlur) { + const color = new TinyColor(e.target.value); setValue(name, (color.isValid ? getColorByFormat(color, format) : undefined) as never); // Need to cast as never as type is too complex } From e1d49adbf849a34e612b1f1549f0c1c93164848d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michele=20Masciav=C3=A8?= Date: Wed, 15 Apr 2026 16:36:51 +0200 Subject: [PATCH 09/18] test --- src/lib/components/ColorPicker/ColorPicker.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/lib/components/ColorPicker/ColorPicker.tsx b/src/lib/components/ColorPicker/ColorPicker.tsx index 30e2c5a..76342c7 100644 --- a/src/lib/components/ColorPicker/ColorPicker.tsx +++ b/src/lib/components/ColorPicker/ColorPicker.tsx @@ -12,6 +12,7 @@ import { TinyColor } from "@ctrl/tinycolor"; import Popover from "@mui/material/Popover"; import Colorful from "@uiw/react-color-colorful"; // must be imported as default, otherwise it will provide a runtime error in nextjs import { getRequiredLabel } from "../../helpers/form"; +import { isNullOrWhitespace } from "@neolution-ch/javascript-utils"; const getColorByFormat = (color: TinyColor, format: ColorPickerInputProps["format"]) => { switch (format) { @@ -98,15 +99,22 @@ const ColorPicker = (props: ColorPickerInputProps) => }} onBlur={(e) => { if (convertColorToFormatOrUndefinedOnBlur) { - const color = new TinyColor(e.target.value); - setValue(name, (color.isValid ? getColorByFormat(color, format) : undefined) as never); // Need to cast as never as type is too complex + const currentValue = e.target.value; + const currentColor = new TinyColor(currentValue); + + if (isNullOrWhitespace(currentValue) || !currentColor.isValid) { + setValue(name, undefined as never, { shouldDirty: true, shouldTouch: true }); + } else { + // Need to cast as never as type is too complex + setValue(name, getColorByFormat(currentColor, format) as never, { shouldDirty: true, shouldTouch: true }); + } + } else { + field.onBlur(); } if (propsOnBlur) { propsOnBlur(e); } - - field.onBlur(); }} value={field.value || ""} slotProps={{ From c586e21fb32ad4c8f14996b1fadac882c0c2315c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michele=20Masciav=C3=A8?= Date: Wed, 15 Apr 2026 17:07:18 +0200 Subject: [PATCH 10/18] test --- .../components/ColorPicker/ColorPicker.tsx | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/lib/components/ColorPicker/ColorPicker.tsx b/src/lib/components/ColorPicker/ColorPicker.tsx index 76342c7..e7fe6cd 100644 --- a/src/lib/components/ColorPicker/ColorPicker.tsx +++ b/src/lib/components/ColorPicker/ColorPicker.tsx @@ -91,32 +91,25 @@ const ColorPicker = (props: ColorPickerInputProps) => disabled={isDisabled} onFocus={focusHandler} onChange={(e) => { - if (propsOnChange) { - propsOnChange(e.target.value); - } + const raw = e.target.value; + propsOnChange?.(raw); - field.onChange(e); + if (isNullOrWhitespace(raw)) { + setValue(name, undefined as never, { shouldDirty: true }); + } else { + field.onChange(e); + } }} onBlur={(e) => { if (convertColorToFormatOrUndefinedOnBlur) { - const currentValue = e.target.value; - const currentColor = new TinyColor(currentValue); - - if (isNullOrWhitespace(currentValue) || !currentColor.isValid) { - setValue(name, undefined as never, { shouldDirty: true, shouldTouch: true }); - } else { - // Need to cast as never as type is too complex - setValue(name, getColorByFormat(currentColor, format) as never, { shouldDirty: true, shouldTouch: true }); - } - } else { - field.onBlur(); - } - - if (propsOnBlur) { - propsOnBlur(e); + const raw = e.target.value; + const color = new TinyColor(raw); + const value = isNullOrWhitespace(raw) || !color.isValid ? undefined : getColorByFormat(color, format); + setValue(name, value as never, { shouldDirty: true, shouldTouch: true }); } + propsOnBlur?.(e); }} - value={field.value || ""} + value={field.value ?? ""} slotProps={{ input: { startAdornment: ( From 37d3cdc8ffadbbfee8f9c3c3b2f971b9227ac8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michele=20Masciav=C3=A8?= Date: Wed, 15 Apr 2026 17:18:30 +0200 Subject: [PATCH 11/18] revert --- CHANGELOG.md | 1 - .../components/ColorPicker/ColorPicker.tsx | 26 +++++++++---------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b2b9df..2cda7cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - `ColorPicker` controlled/uncontrolled state warning by ensuring value prop is always defined. -- `ColorPicker` reset value when `convertColorToFormatOrUndefinedOnBlur` is enabled. ## [4.1.0] - 2026-03-16 diff --git a/src/lib/components/ColorPicker/ColorPicker.tsx b/src/lib/components/ColorPicker/ColorPicker.tsx index e7fe6cd..dbb30d9 100644 --- a/src/lib/components/ColorPicker/ColorPicker.tsx +++ b/src/lib/components/ColorPicker/ColorPicker.tsx @@ -12,7 +12,6 @@ import { TinyColor } from "@ctrl/tinycolor"; import Popover from "@mui/material/Popover"; import Colorful from "@uiw/react-color-colorful"; // must be imported as default, otherwise it will provide a runtime error in nextjs import { getRequiredLabel } from "../../helpers/form"; -import { isNullOrWhitespace } from "@neolution-ch/javascript-utils"; const getColorByFormat = (color: TinyColor, format: ColorPickerInputProps["format"]) => { switch (format) { @@ -91,23 +90,22 @@ const ColorPicker = (props: ColorPickerInputProps) => disabled={isDisabled} onFocus={focusHandler} onChange={(e) => { - const raw = e.target.value; - propsOnChange?.(raw); - - if (isNullOrWhitespace(raw)) { - setValue(name, undefined as never, { shouldDirty: true }); - } else { - field.onChange(e); + if (propsOnChange) { + propsOnChange(e.target.value); } + + field.onChange(e); }} onBlur={(e) => { if (convertColorToFormatOrUndefinedOnBlur) { - const raw = e.target.value; - const color = new TinyColor(raw); - const value = isNullOrWhitespace(raw) || !color.isValid ? undefined : getColorByFormat(color, format); - setValue(name, value as never, { shouldDirty: true, shouldTouch: true }); + setValue(name, (color.isValid ? getColorByFormat(color, format) : undefined) as never); // Need to cast as never as type is too complex } - propsOnBlur?.(e); + + if (propsOnBlur) { + propsOnBlur(e); + } + + field.onBlur(); }} value={field.value ?? ""} slotProps={{ @@ -160,4 +158,4 @@ const ColorPicker = (props: ColorPickerInputProps) => ); }; -export { ColorPicker }; +export { ColorPicker }; \ No newline at end of file From fb77a23fe1dd6ac4226ebc3be96bdd1eb5cccce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michele=20Masciav=C3=A8?= Date: Wed, 15 Apr 2026 17:18:57 +0200 Subject: [PATCH 12/18] x --- src/lib/components/ColorPicker/ColorPicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/ColorPicker/ColorPicker.tsx b/src/lib/components/ColorPicker/ColorPicker.tsx index dbb30d9..f00a828 100644 --- a/src/lib/components/ColorPicker/ColorPicker.tsx +++ b/src/lib/components/ColorPicker/ColorPicker.tsx @@ -158,4 +158,4 @@ const ColorPicker = (props: ColorPickerInputProps) => ); }; -export { ColorPicker }; \ No newline at end of file +export { ColorPicker }; From a1d30ef52bf6c21b6c0d0ca0db2d665b054a6be1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michele=20Masciav=C3=A8?= Date: Wed, 15 Apr 2026 17:40:15 +0200 Subject: [PATCH 13/18] test --- src/lib/components/ColorPicker/ColorPicker.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib/components/ColorPicker/ColorPicker.tsx b/src/lib/components/ColorPicker/ColorPicker.tsx index f00a828..ccd64ad 100644 --- a/src/lib/components/ColorPicker/ColorPicker.tsx +++ b/src/lib/components/ColorPicker/ColorPicker.tsx @@ -98,14 +98,18 @@ const ColorPicker = (props: ColorPickerInputProps) => }} onBlur={(e) => { if (convertColorToFormatOrUndefinedOnBlur) { - setValue(name, (color.isValid ? getColorByFormat(color, format) : undefined) as never); // Need to cast as never as type is too complex + // Need to cast as never as type is too complex + setValue(name, (color.isValid ? getColorByFormat(color, format) : undefined) as never, { + shouldDirty: true, + shouldTouch: true, + }); + } else { + field.onBlur(); } if (propsOnBlur) { propsOnBlur(e); } - - field.onBlur(); }} value={field.value ?? ""} slotProps={{ From f495ce02a75f56e249385ca317af14d52ddc0718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michele=20Masciav=C3=A8?= Date: Wed, 15 Apr 2026 17:49:03 +0200 Subject: [PATCH 14/18] x --- src/lib/components/ColorPicker/ColorPicker.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/components/ColorPicker/ColorPicker.tsx b/src/lib/components/ColorPicker/ColorPicker.tsx index ccd64ad..6f0528d 100644 --- a/src/lib/components/ColorPicker/ColorPicker.tsx +++ b/src/lib/components/ColorPicker/ColorPicker.tsx @@ -99,7 +99,8 @@ const ColorPicker = (props: ColorPickerInputProps) => onBlur={(e) => { if (convertColorToFormatOrUndefinedOnBlur) { // Need to cast as never as type is too complex - setValue(name, (color.isValid ? getColorByFormat(color, format) : undefined) as never, { + const newColor = new TinyColor(e.target.value); + setValue(name, (newColor.isValid ? getColorByFormat(newColor, format) : undefined) as never, { shouldDirty: true, shouldTouch: true, }); From b956d516d67a3918c57f57789c20f911975dd652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michele=20Masciav=C3=A8?= Date: Wed, 15 Apr 2026 18:06:38 +0200 Subject: [PATCH 15/18] revert --- src/lib/components/ColorPicker/ColorPicker.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/lib/components/ColorPicker/ColorPicker.tsx b/src/lib/components/ColorPicker/ColorPicker.tsx index 6f0528d..f00a828 100644 --- a/src/lib/components/ColorPicker/ColorPicker.tsx +++ b/src/lib/components/ColorPicker/ColorPicker.tsx @@ -98,19 +98,14 @@ const ColorPicker = (props: ColorPickerInputProps) => }} onBlur={(e) => { if (convertColorToFormatOrUndefinedOnBlur) { - // Need to cast as never as type is too complex - const newColor = new TinyColor(e.target.value); - setValue(name, (newColor.isValid ? getColorByFormat(newColor, format) : undefined) as never, { - shouldDirty: true, - shouldTouch: true, - }); - } else { - field.onBlur(); + setValue(name, (color.isValid ? getColorByFormat(color, format) : undefined) as never); // Need to cast as never as type is too complex } if (propsOnBlur) { propsOnBlur(e); } + + field.onBlur(); }} value={field.value ?? ""} slotProps={{ From d60f2b9684091c9d522ed1cab3bd65726f1adb12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michele=20Masciav=C3=A8?= Date: Wed, 15 Apr 2026 18:28:09 +0200 Subject: [PATCH 16/18] test --- src/lib/components/ColorPicker/ColorPicker.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/components/ColorPicker/ColorPicker.tsx b/src/lib/components/ColorPicker/ColorPicker.tsx index f00a828..73c6d4e 100644 --- a/src/lib/components/ColorPicker/ColorPicker.tsx +++ b/src/lib/components/ColorPicker/ColorPicker.tsx @@ -47,6 +47,7 @@ const ColorPicker = (props: ColorPickerInputProps) => disabled: formDisabled, getFieldState, setValue, + watch, requiredFields, formState: { errors }, hideValidationMessages, @@ -65,7 +66,8 @@ const ColorPicker = (props: ColorPickerInputProps) => }); const isDisabled = formDisabled || disabled; - const color = useMemo(() => new TinyColor(field.value), [field.value]); + const fieldValue = watch(name) as string | undefined; + const color = useMemo(() => new TinyColor(fieldValue), [fieldValue]); const fieldError = get(errors, name) as FieldError | undefined; const hideErrorMessage = useMemo(() => hideValidationMessages || hideValidationMessage, [hideValidationMessages, hideValidationMessage]); const hasError = useMemo(() => !!fieldError, [fieldError]); From bd0a43ad883407c8cd0459e7ae15155d9e01234c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michele=20Masciav=C3=A8?= Date: Wed, 15 Apr 2026 18:43:54 +0200 Subject: [PATCH 17/18] test --- src/lib/components/ColorPicker/ColorPicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/ColorPicker/ColorPicker.tsx b/src/lib/components/ColorPicker/ColorPicker.tsx index 73c6d4e..6ba7547 100644 --- a/src/lib/components/ColorPicker/ColorPicker.tsx +++ b/src/lib/components/ColorPicker/ColorPicker.tsx @@ -109,7 +109,7 @@ const ColorPicker = (props: ColorPickerInputProps) => field.onBlur(); }} - value={field.value ?? ""} + value={fieldValue ?? ""} slotProps={{ input: { startAdornment: ( From 8579cc1f9fdf4796076a11238d1e579ca6650f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michele=20Masciav=C3=A8?= Date: Wed, 15 Apr 2026 18:49:37 +0200 Subject: [PATCH 18/18] x --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cda7cb..74dd1c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - `ColorPicker` controlled/uncontrolled state warning by ensuring value prop is always defined. +- `ColorPicker` clear value when field value is already set. ## [4.1.0] - 2026-03-16