@fuf-stack/uniform 1.22.3 → 1.22.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Checkboxes/index.cjs +2 -1
- package/dist/Checkboxes/index.d.cts +2 -2
- package/dist/Checkboxes/index.d.ts +2 -2
- package/dist/Checkboxes/index.js +2 -2
- package/dist/{Checkboxes-DHk09GmH.cjs → Checkboxes-DrJqx6O2.cjs} +7 -1
- package/dist/{Checkboxes-DHk09GmH.cjs.map → Checkboxes-DrJqx6O2.cjs.map} +1 -1
- package/dist/{Checkboxes-D1hppxDE.js → Checkboxes-X6FRwI50.js} +2 -2
- package/dist/{Checkboxes-D1hppxDE.js.map → Checkboxes-X6FRwI50.js.map} +1 -1
- package/dist/DatePicker/index.cjs +2 -1
- package/dist/DatePicker/index.d.cts +2 -2
- package/dist/DatePicker/index.d.ts +2 -2
- package/dist/DatePicker/index.js +2 -2
- package/dist/{DatePicker-BUSi8SWQ.js → DatePicker-CGIWizz9.js} +2 -2
- package/dist/{DatePicker-BUSi8SWQ.js.map → DatePicker-CGIWizz9.js.map} +1 -1
- package/dist/{DatePicker-DuBjvS5g.cjs → DatePicker-CYUuwwsm.cjs} +7 -1
- package/dist/{DatePicker-DuBjvS5g.cjs.map → DatePicker-CYUuwwsm.cjs.map} +1 -1
- package/dist/FieldArray/index.cjs +2 -1
- package/dist/FieldArray/index.d.cts +2 -2
- package/dist/FieldArray/index.d.ts +2 -2
- package/dist/FieldArray/index.js +2 -2
- package/dist/{FieldArray-BxmK1NRd.js → FieldArray-Dkw6JBnT.js} +2 -2
- package/dist/{FieldArray-BxmK1NRd.js.map → FieldArray-Dkw6JBnT.js.map} +1 -1
- package/dist/{FieldArray-DgiQpIdr.cjs → FieldArray-V66fsvtd.cjs} +7 -1
- package/dist/{FieldArray-DgiQpIdr.cjs.map → FieldArray-V66fsvtd.cjs.map} +1 -1
- package/dist/FieldCard/index.cjs +6 -2
- package/dist/FieldCard/index.d.cts +2 -2
- package/dist/FieldCard/index.d.ts +2 -2
- package/dist/FieldCard/index.js +2 -2
- package/dist/{FieldCard-C-DZ5ARr.cjs → FieldCard-CAHnM8C0.cjs} +10 -1
- package/dist/FieldCard-CAHnM8C0.cjs.map +1 -0
- package/dist/{FieldCard-BK6SNqFY.js → FieldCard-Cg31ce9J.js} +5 -2
- package/dist/FieldCard-Cg31ce9J.js.map +1 -0
- package/dist/Input/index.d.cts +1 -1
- package/dist/Input/index.d.ts +1 -1
- package/dist/RadioBoxes/index.cjs +1 -1
- package/dist/RadioBoxes/index.d.cts +2 -2
- package/dist/RadioBoxes/index.d.ts +2 -2
- package/dist/RadioBoxes/index.js +1 -1
- package/dist/{RadioBoxes-CR--FBhQ.cjs → RadioBoxes-BmLrbYXt.cjs} +3 -3
- package/dist/RadioBoxes-BmLrbYXt.cjs.map +1 -0
- package/dist/{RadioBoxes-BJz-aJp3.js → RadioBoxes-DWxJBTKR.js} +3 -3
- package/dist/RadioBoxes-DWxJBTKR.js.map +1 -0
- package/dist/RadioTabs/index.cjs +2 -1
- package/dist/RadioTabs/index.d.cts +2 -2
- package/dist/RadioTabs/index.d.ts +2 -2
- package/dist/RadioTabs/index.js +2 -2
- package/dist/{RadioTabs-Btt582Nl.cjs → RadioTabs-14XTbTJU.cjs} +20 -14
- package/dist/RadioTabs-14XTbTJU.cjs.map +1 -0
- package/dist/{RadioTabs-CYmzrPBG.js → RadioTabs-DAY6MYJI.js} +15 -15
- package/dist/RadioTabs-DAY6MYJI.js.map +1 -0
- package/dist/Radios/index.cjs +2 -1
- package/dist/Radios/index.d.cts +2 -2
- package/dist/Radios/index.d.ts +2 -2
- package/dist/Radios/index.js +2 -2
- package/dist/{Radios-DZxhrX96.js → Radios-C938-msm.js} +2 -2
- package/dist/{Radios-DZxhrX96.js.map → Radios-C938-msm.js.map} +1 -1
- package/dist/{Radios-Jg9mwE0B.cjs → Radios-D_B9Y8s2.cjs} +7 -1
- package/dist/{Radios-Jg9mwE0B.cjs.map → Radios-D_B9Y8s2.cjs.map} +1 -1
- package/dist/Select/index.cjs +2 -1
- package/dist/Select/index.d.cts +2 -2
- package/dist/Select/index.d.ts +2 -2
- package/dist/Select/index.js +2 -2
- package/dist/{Select-CgeqhZIC.js → Select-BL6k_e-D.js} +2 -2
- package/dist/Select-BL6k_e-D.js.map +1 -0
- package/dist/{Select-CZwuEAB5.cjs → Select-D66A-hYm.cjs} +7 -1
- package/dist/Select-D66A-hYm.cjs.map +1 -0
- package/dist/Slider/index.cjs +2 -1
- package/dist/Slider/index.d.cts +2 -2
- package/dist/Slider/index.d.ts +2 -2
- package/dist/Slider/index.js +2 -2
- package/dist/{Slider-Bpxt-Qgj.cjs → Slider-ChC2PZNb.cjs} +7 -1
- package/dist/{Slider-Bpxt-Qgj.cjs.map → Slider-ChC2PZNb.cjs.map} +1 -1
- package/dist/{Slider-DRfSw0uE.js → Slider-DmEwhC1T.js} +2 -2
- package/dist/{Slider-DRfSw0uE.js.map → Slider-DmEwhC1T.js.map} +1 -1
- package/dist/Switch/index.cjs +2 -1
- package/dist/Switch/index.d.cts +2 -2
- package/dist/Switch/index.d.ts +2 -2
- package/dist/Switch/index.js +2 -2
- package/dist/{Switch-hjjy34QB.cjs → Switch-Ch6_VInV.cjs} +7 -1
- package/dist/{Switch-hjjy34QB.cjs.map → Switch-Ch6_VInV.cjs.map} +1 -1
- package/dist/{Switch-D9DNrqmj.js → Switch-DvKRPFcC.js} +2 -2
- package/dist/{Switch-D9DNrqmj.js.map → Switch-DvKRPFcC.js.map} +1 -1
- package/dist/SwitchBox/index.d.cts +1 -1
- package/dist/SwitchBox/index.d.ts +1 -1
- package/dist/SwitchBox-BU1XieaZ.js.map +1 -1
- package/dist/SwitchBox-Dl-F5y2m.cjs.map +1 -1
- package/dist/TextArea/index.d.cts +1 -1
- package/dist/TextArea/index.d.ts +1 -1
- package/dist/Time/index.cjs +7 -2
- package/dist/Time/index.d.cts +2 -2
- package/dist/Time/index.d.ts +2 -2
- package/dist/Time/index.js +2 -2
- package/dist/{Time-CDptQ3CK.js → Time-B_SXrKWK.js} +5 -2
- package/dist/Time-B_SXrKWK.js.map +1 -0
- package/dist/{Time-WKgbji5k.cjs → Time-CYoWaQsz.cjs} +16 -1
- package/dist/Time-CYoWaQsz.cjs.map +1 -0
- package/dist/hooks/useUniformFieldArray/index.cjs +4 -4
- package/dist/hooks/useUniformFieldArray/index.cjs.map +1 -1
- package/dist/hooks/useUniformFieldArray/index.js +4 -4
- package/dist/hooks/useUniformFieldArray/index.js.map +1 -1
- package/dist/index-19JGtN7H.d.cts +1257 -0
- package/dist/index-19JGtN7H.d.cts.map +1 -0
- package/dist/index-B4XExHDi.d.cts +539 -0
- package/dist/index-B4XExHDi.d.cts.map +1 -0
- package/dist/index-BUZEnZDm.d.ts +1760 -0
- package/dist/index-BUZEnZDm.d.ts.map +1 -0
- package/dist/index-Bbaubtxp.d.cts +677 -0
- package/dist/index-Bbaubtxp.d.cts.map +1 -0
- package/dist/index-BdgFJ-dj.d.ts +1028 -0
- package/dist/index-BdgFJ-dj.d.ts.map +1 -0
- package/dist/index-BiZNkLK3.d.cts +1477 -0
- package/dist/index-BiZNkLK3.d.cts.map +1 -0
- package/dist/index-Bie3CWyW.d.cts +1760 -0
- package/dist/index-Bie3CWyW.d.cts.map +1 -0
- package/dist/index-BsTqcI1C.d.cts +659 -0
- package/dist/index-BsTqcI1C.d.cts.map +1 -0
- package/dist/index-C6Y8KybK.d.ts +677 -0
- package/dist/index-C6Y8KybK.d.ts.map +1 -0
- package/dist/index-CD0Wpla3.d.cts +1413 -0
- package/dist/index-CD0Wpla3.d.cts.map +1 -0
- package/dist/index-CUByIf_d.d.ts +314 -0
- package/dist/index-CUByIf_d.d.ts.map +1 -0
- package/dist/index-Cf2B9woY.d.cts +473 -0
- package/dist/index-Cf2B9woY.d.cts.map +1 -0
- package/dist/index-CqXEYILn.d.cts +1263 -0
- package/dist/index-CqXEYILn.d.cts.map +1 -0
- package/dist/index-D1cB3mbB.d.ts +3917 -0
- package/dist/index-D1cB3mbB.d.ts.map +1 -0
- package/dist/index-D7AUghFx.d.ts +1413 -0
- package/dist/index-D7AUghFx.d.ts.map +1 -0
- package/dist/index-DCwffq1f.d.ts +1263 -0
- package/dist/index-DCwffq1f.d.ts.map +1 -0
- package/dist/index-DFbuWomg.d.ts +534 -0
- package/dist/index-DFbuWomg.d.ts.map +1 -0
- package/dist/index-DXOBPBft.d.ts +659 -0
- package/dist/index-DXOBPBft.d.ts.map +1 -0
- package/dist/index-DY-d4cFe.d.cts +1028 -0
- package/dist/index-DY-d4cFe.d.cts.map +1 -0
- package/dist/index-DqMpHpu7.d.ts +1257 -0
- package/dist/index-DqMpHpu7.d.ts.map +1 -0
- package/dist/index-DwnqyNnX.d.cts +534 -0
- package/dist/index-DwnqyNnX.d.cts.map +1 -0
- package/dist/index-OdNTqjhQ.d.ts +585 -0
- package/dist/index-OdNTqjhQ.d.ts.map +1 -0
- package/dist/index-QPDvw5Z5.d.cts +585 -0
- package/dist/index-QPDvw5Z5.d.cts.map +1 -0
- package/dist/index-amMd-sQN.d.ts +473 -0
- package/dist/index-amMd-sQN.d.ts.map +1 -0
- package/dist/index-clztdahp.d.ts +1477 -0
- package/dist/index-clztdahp.d.ts.map +1 -0
- package/dist/index-jK092-MJ.d.cts +314 -0
- package/dist/index-jK092-MJ.d.cts.map +1 -0
- package/dist/index-veBM95Gw.d.cts +3917 -0
- package/dist/index-veBM95Gw.d.cts.map +1 -0
- package/dist/index-yYjVSHlN.d.ts +539 -0
- package/dist/index-yYjVSHlN.d.ts.map +1 -0
- package/dist/index.cjs +20 -11
- package/dist/index.d.cts +15 -15
- package/dist/index.d.ts +15 -15
- package/dist/index.js +12 -12
- package/package.json +6 -6
- package/dist/FieldCard-BK6SNqFY.js.map +0 -1
- package/dist/FieldCard-C-DZ5ARr.cjs.map +0 -1
- package/dist/RadioBoxes-BJz-aJp3.js.map +0 -1
- package/dist/RadioBoxes-CR--FBhQ.cjs.map +0 -1
- package/dist/RadioTabs-Btt582Nl.cjs.map +0 -1
- package/dist/RadioTabs-CYmzrPBG.js.map +0 -1
- package/dist/Select-CZwuEAB5.cjs.map +0 -1
- package/dist/Select-CgeqhZIC.js.map +0 -1
- package/dist/Time-CDptQ3CK.js.map +0 -1
- package/dist/Time-WKgbji5k.cjs.map +0 -1
- package/dist/index-B4jKGy-P.d.ts +0 -167
- package/dist/index-B4jKGy-P.d.ts.map +0 -1
- package/dist/index-B5SfdA5O.d.cts +0 -99
- package/dist/index-B5SfdA5O.d.cts.map +0 -1
- package/dist/index-B61zRzeS.d.cts +0 -101
- package/dist/index-B61zRzeS.d.cts.map +0 -1
- package/dist/index-BDFJDKmz.d.ts +0 -166
- package/dist/index-BDFJDKmz.d.ts.map +0 -1
- package/dist/index-BE4OAqJ2.d.cts +0 -113
- package/dist/index-BE4OAqJ2.d.cts.map +0 -1
- package/dist/index-BMEKfOWp.d.ts +0 -174
- package/dist/index-BMEKfOWp.d.ts.map +0 -1
- package/dist/index-BPaSE_7b.d.ts +0 -179
- package/dist/index-BPaSE_7b.d.ts.map +0 -1
- package/dist/index-BSjcTo7A.d.ts +0 -147
- package/dist/index-BSjcTo7A.d.ts.map +0 -1
- package/dist/index-Bgo2Dr6X.d.cts +0 -174
- package/dist/index-Bgo2Dr6X.d.cts.map +0 -1
- package/dist/index-BmMspnsJ.d.cts +0 -137
- package/dist/index-BmMspnsJ.d.cts.map +0 -1
- package/dist/index-BvbHkoH5.d.cts +0 -290
- package/dist/index-BvbHkoH5.d.cts.map +0 -1
- package/dist/index-C6DBVs7t.d.ts +0 -116
- package/dist/index-C6DBVs7t.d.ts.map +0 -1
- package/dist/index-CIiOXrrY.d.ts +0 -99
- package/dist/index-CIiOXrrY.d.ts.map +0 -1
- package/dist/index-CSRKNes5.d.cts +0 -167
- package/dist/index-CSRKNes5.d.cts.map +0 -1
- package/dist/index-CbPJiRK2.d.ts +0 -83
- package/dist/index-CbPJiRK2.d.ts.map +0 -1
- package/dist/index-CbQ8Ft4o.d.ts +0 -113
- package/dist/index-CbQ8Ft4o.d.ts.map +0 -1
- package/dist/index-CvP3tbVV.d.ts +0 -137
- package/dist/index-CvP3tbVV.d.ts.map +0 -1
- package/dist/index-Cwb7sTO0.d.ts +0 -154
- package/dist/index-Cwb7sTO0.d.ts.map +0 -1
- package/dist/index-CxY9VWv5.d.cts +0 -154
- package/dist/index-CxY9VWv5.d.cts.map +0 -1
- package/dist/index-Cz4JaCWd.d.ts +0 -122
- package/dist/index-Cz4JaCWd.d.ts.map +0 -1
- package/dist/index-DBsF7xsq.d.cts +0 -147
- package/dist/index-DBsF7xsq.d.cts.map +0 -1
- package/dist/index-DMOhTl4I.d.cts +0 -116
- package/dist/index-DMOhTl4I.d.cts.map +0 -1
- package/dist/index-DRg0anjB.d.cts +0 -179
- package/dist/index-DRg0anjB.d.cts.map +0 -1
- package/dist/index-DUmNI6cE.d.cts +0 -83
- package/dist/index-DUmNI6cE.d.cts.map +0 -1
- package/dist/index-Db0WD4d1.d.cts +0 -166
- package/dist/index-Db0WD4d1.d.cts.map +0 -1
- package/dist/index-GqydEAFk.d.ts +0 -290
- package/dist/index-GqydEAFk.d.ts.map +0 -1
- package/dist/index-PMCqBX0t.d.ts +0 -101
- package/dist/index-PMCqBX0t.d.ts.map +0 -1
- package/dist/index-Q3BWGyUO.d.cts +0 -122
- package/dist/index-Q3BWGyUO.d.cts.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Time-CYoWaQsz.cjs","names":["useUniformField","HeroTimeInput"],"sources":["../src/Time/timeHelpers.ts","../src/Time/Time.tsx","../src/Time/index.ts"],"sourcesContent":["import type { TimeInput as HeroTimeInput } from '@heroui/date-input';\nimport type { ComponentProps } from 'react';\n\nimport {\n getLocalTimeZone,\n parseAbsolute,\n parseAbsoluteToLocal,\n parseDateTime,\n parseZonedDateTime,\n} from '@internationalized/date';\n\n/** Value type accepted by HeroUI TimeInput */\nexport type TimeValue = ComponentProps<typeof HeroTimeInput>['value'];\n\n/** Cast unknown input to the TimeInput value union type. */\nconst asTimeValue = (value: unknown): TimeValue => {\n return value as TimeValue;\n};\n\n/** Check whether a value already looks like an @internationalized/date value. */\nconst isTimeValueLike = (value: unknown): value is object => {\n if (!value || typeof value !== 'object') {\n return false;\n }\n\n return 'hour' in value || 'calendar' in value;\n};\n\n/** Check if the incoming string represents a plain time (e.g. 09:30 or 09:30:45). */\nconst isTimeOnlyString = (value: string): boolean => {\n return /^\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d{1,3})?)?$/.test(value);\n};\n\n/** Check if the incoming string is a UTC time-only value (e.g. 09:30Z). */\nconst isUtcTimeOnlyString = (value: string): boolean => {\n return /^\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d{1,3})?)?Z$/.test(value);\n};\n\n/** Zero-pad integer segments for time serialization. */\nconst pad = (value: number): string => {\n return String(value).padStart(2, '0');\n};\n\n/** Convert an absolute ISO string into UTC time-only store format. */\nconst toUtcTimeString = (\n absoluteIsoString: string,\n granularity: 'hour' | 'minute',\n): string | null => {\n const utcDate = new Date(absoluteIsoString);\n if (Number.isNaN(utcDate.getTime())) {\n return null;\n }\n\n const hour = pad(utcDate.getUTCHours());\n if (granularity === 'hour') {\n return `${hour}:00Z`;\n }\n\n return `${hour}:${pad(utcDate.getUTCMinutes())}Z`;\n};\n\n/** True when an input already carries timezone/offset information. */\nconst hasExplicitTimeZone = (value: string): boolean => {\n if (value.includes('[') && value.includes(']')) {\n return true;\n }\n\n return /(?:[zZ]|[+-]\\d{2}:\\d{2})$/.test(value);\n};\n\n/** True for date-time strings without timezone/offset. */\nconst isNaiveDateTimeString = (value: string): boolean => {\n return /^\\d{4}-\\d{2}-\\d{2}T/.test(value) && !hasExplicitTimeZone(value);\n};\n\n/** Safely run a parser and return `null` on parsing errors. */\nconst tryParse = (parser: () => unknown): TimeValue => {\n try {\n return asTimeValue(parser());\n } catch {\n return null;\n }\n};\n\n/**\n * Parse a form value into a HeroUI TimeInput compatible `TimeValue`.\n *\n * Supports existing `TimeValue` objects, native `Date` instances, plain time\n * strings (`HH:mm[:ss]`), absolute ISO date-time strings, zoned date-time\n * strings, and local date-time strings.\n *\n * Time-only strings are interpreted in the active timezone and normalized to a\n * `ZonedDateTime` (using a fixed reference date) so timezone abbreviations can\n * remain visible in the UI.\n */\nexport const parseTimeValue = (\n value: unknown,\n timeZone?: string,\n): TimeValue => {\n if (value == null || value === '') {\n return null;\n }\n\n if (isTimeValueLike(value)) {\n return asTimeValue(value);\n }\n\n if (value instanceof Date) {\n return asTimeValue(\n timeZone\n ? parseAbsolute(value.toISOString(), timeZone)\n : parseAbsoluteToLocal(value.toISOString()),\n );\n }\n\n if (typeof value !== 'string') {\n return null;\n }\n\n const normalizedValue = value.trim();\n if (normalizedValue === '') {\n return null;\n }\n\n if (isTimeOnlyString(normalizedValue)) {\n return tryParse(() => {\n const [hourValue, minuteValue, secondValue] = normalizedValue\n .split(':')\n .map(Number);\n const resolvedTimeZone = resolveTimeFieldTimeZone(timeZone);\n const hour = pad(hourValue);\n const minute = pad(minuteValue);\n const second = pad(secondValue ?? 0);\n\n return parseZonedDateTime(\n `1970-01-01T${hour}:${minute}:${second}[${resolvedTimeZone}]`,\n );\n });\n }\n\n if (isUtcTimeOnlyString(normalizedValue)) {\n return tryParse(() => {\n const resolvedTimeZone = resolveTimeFieldTimeZone(timeZone);\n return parseAbsolute(`1970-01-01T${normalizedValue}`, resolvedTimeZone);\n });\n }\n\n const parsers: (() => unknown)[] = [\n ...(timeZone && isNaiveDateTimeString(normalizedValue)\n ? [\n () => {\n return parseZonedDateTime(`${normalizedValue}[${timeZone}]`);\n },\n ]\n : []),\n () => {\n return timeZone\n ? parseAbsolute(normalizedValue, timeZone)\n : parseAbsoluteToLocal(normalizedValue);\n },\n () => {\n return parseZonedDateTime(normalizedValue);\n },\n () => {\n return parseDateTime(normalizedValue);\n },\n ];\n\n return (\n parsers\n .map((parser) => {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n return tryParse(parser);\n })\n .find((parsedValue) => {\n return parsedValue != null;\n }) ?? null\n );\n};\n\n/** Resolve the timezone used for parsing and string conversion. */\nexport const resolveTimeFieldTimeZone = (timeZone?: string): string => {\n return timeZone ?? getLocalTimeZone();\n};\n\n/**\n * Build default placeholder value for TimeInput.\n *\n * Uses a fixed reference time to keep snapshots deterministic.\n */\nexport const getTimeFieldPlaceholderValue = (timeZone?: string): TimeValue => {\n const resolvedTimeZone = resolveTimeFieldTimeZone(timeZone);\n return asTimeValue(\n parseZonedDateTime(`1970-01-01T00:00:00[${resolvedTimeZone}]`),\n );\n};\n\n/**\n * Convert a TimeValue into a UTC time-only string for form state.\n *\n * The stored format is `HH:mmZ` (or `HH:00Z` when granularity is `hour`).\n */\nexport const timeValueToString = (\n value: TimeValue,\n timeZone: string,\n granularity: 'hour' | 'minute' = 'minute',\n): string | null => {\n if (value == null) {\n return null;\n }\n\n const maybeWithAbsoluteString = value as { toAbsoluteString?: () => string };\n if (typeof maybeWithAbsoluteString.toAbsoluteString === 'function') {\n return toUtcTimeString(\n maybeWithAbsoluteString.toAbsoluteString(),\n granularity,\n );\n }\n\n const maybeWithToDate = value as { toDate?: (zone: string) => Date };\n if (typeof maybeWithToDate.toDate === 'function') {\n return toUtcTimeString(\n maybeWithToDate.toDate(timeZone).toISOString(),\n granularity,\n );\n }\n\n const maybeTime = value as {\n hour?: number;\n minute?: number;\n second?: number;\n };\n if (\n typeof maybeTime.hour === 'number' &&\n typeof maybeTime.minute === 'number'\n ) {\n const hour = pad(maybeTime.hour);\n const minute = pad(maybeTime.minute);\n const second = pad(maybeTime.second ?? 0);\n return toUtcTimeString(\n parseZonedDateTime(\n `1970-01-01T${hour}:${minute}:${second}[${timeZone}]`,\n ).toAbsoluteString(),\n granularity,\n );\n }\n\n return null;\n};\n","import type { TVClassName, TVProps } from '@fuf-stack/pixel-utils';\nimport type { ReactNode } from 'react';\nimport type { TimeValue } from './timeHelpers';\n\nimport { TimeInput as HeroTimeInput } from '@heroui/date-input';\n\nimport { tv, variantsToClassNames } from '@fuf-stack/pixel-utils';\n\nimport { useUniformField } from '../hooks/useUniformField';\nimport {\n getTimeFieldPlaceholderValue,\n parseTimeValue,\n resolveTimeFieldTimeZone,\n timeValueToString,\n} from './timeHelpers';\n\nexport const timeVariants = tv({\n slots: {\n /** wrapper around the whole time input */\n base: '',\n /** helper wrapper for error/description */\n helperWrapper: [\n // set padding to 0 for error message exit animation\n 'p-0',\n ],\n /** the segmented input element */\n input: '',\n /** inner wrapper around segments and optional content */\n innerWrapper: '',\n /** outer input wrapper */\n inputWrapper: 'bg-content1 focus-within:border-focus',\n /** field label */\n label: '',\n /** individual segment */\n segment: '',\n },\n});\n\ntype VariantProps = TVProps<typeof timeVariants>;\ntype ClassName = TVClassName<typeof timeVariants>;\n\nexport interface TimeProps extends VariantProps {\n /** Custom aria-label for accessibility. If not provided, falls back to field name when no visible label exists */\n ariaLabel?: string;\n /** CSS class name */\n className?: ClassName;\n /** input is disabled */\n disabled?: boolean;\n /** smallest visible unit in the segmented time input */\n granularity?: 'hour' | 'minute';\n /** hide timezone abbreviation for zoned values */\n hideTimeZone?: boolean;\n /** force hour granularity and store UTC hour as number (0-23) */\n hourAsNumber?: boolean;\n /** hour cycle formatting */\n hourCycle?: 12 | 24;\n /** form field label */\n label?: ReactNode;\n /** form field name */\n name: string;\n /** HTML data-testid attribute used in e2e tests */\n testId?: string;\n /**\n * Optional timezone used for parsing date-time strings and serializing values.\n * When omitted, local timezone is used.\n */\n timeZone?: string;\n /** value format stored in form state (`string` stores UTC minute values, e.g. `10:15Z`) */\n valueType?: 'string' | 'timeValue';\n}\n\n/**\n * Time component based on [HeroUI TimeInput](https://v2.heroui.com/docs/components/time-input)\n */\nconst Time = ({\n className: _className = undefined,\n granularity = 'minute',\n hideTimeZone = false,\n hourAsNumber = false,\n hourCycle = undefined,\n name,\n timeZone = undefined,\n valueType = 'string',\n ...uniformFieldProps\n}: TimeProps) => {\n const {\n ariaLabel,\n disabled,\n errorMessage,\n field: { onBlur, onChange, value },\n invalid,\n label,\n required,\n testId,\n } = useUniformField({ name, ...uniformFieldProps });\n\n // classNames from slots\n const variants = timeVariants();\n const classNames = variantsToClassNames(variants, _className, 'base');\n const resolvedTimeZone = resolveTimeFieldTimeZone(timeZone);\n const effectiveGranularity = hourAsNumber ? 'hour' : granularity;\n\n // `hourAsNumber` stores 0-23; convert this into a parseable UTC time for display.\n const normalizedFieldValue =\n hourAsNumber && typeof value === 'number'\n ? `${String(Math.max(0, Math.min(23, value))).padStart(2, '0')}:00Z`\n : value;\n\n // Normalize incoming form value into HeroUI TimeInput value shape.\n const parsedValue = parseTimeValue(normalizedFieldValue, timeZone);\n const placeholderValue = getTimeFieldPlaceholderValue(timeZone);\n\n // Normalize TimeInput output into the configured storage format.\n const handleChange = (nextValue: TimeValue) => {\n if (nextValue == null) {\n onChange(null);\n return;\n }\n\n if (hourAsNumber) {\n const serializedValue = timeValueToString(\n nextValue,\n resolvedTimeZone,\n 'hour',\n );\n\n if (!serializedValue) {\n onChange(null);\n return;\n }\n\n const [hoursSegment] = serializedValue.split(':');\n const parsedHours = Number(hoursSegment);\n onChange(Number.isNaN(parsedHours) ? null : parsedHours);\n return;\n }\n\n if (valueType === 'timeValue') {\n onChange(nextValue);\n return;\n }\n\n onChange(\n timeValueToString(nextValue, resolvedTimeZone, effectiveGranularity) ??\n String(nextValue),\n );\n };\n\n return (\n <HeroTimeInput\n aria-label={label ? undefined : ariaLabel}\n classNames={{\n base: classNames.base,\n helperWrapper: classNames.helperWrapper,\n innerWrapper: classNames.innerWrapper,\n input: classNames.input,\n inputWrapper: classNames.inputWrapper,\n label: classNames.label,\n segment: classNames.segment,\n }}\n data-testid={testId}\n errorMessage={errorMessage}\n granularity={effectiveGranularity}\n hideTimeZone={hideTimeZone}\n hourCycle={hourCycle}\n id={testId}\n isDisabled={disabled}\n isInvalid={invalid}\n isRequired={required}\n label={label}\n labelPlacement=\"outside\"\n name={name}\n onBlur={onBlur}\n onChange={handleChange}\n placeholderValue={placeholderValue ?? undefined}\n radius=\"sm\"\n value={parsedValue ?? undefined}\n variant=\"bordered\"\n />\n );\n};\n\nexport default Time;\n","import Time from './Time';\n\nexport type { TimeProps } from './Time';\n\nexport { timeVariants } from './Time';\n\nexport { Time };\n\nexport default Time;\n"],"mappings":";;;;;;;;AAeA,MAAM,eAAe,UAA8B;CACjD,OAAO;AACT;;AAGA,MAAM,mBAAmB,UAAoC;CAC3D,IAAI,CAAC,SAAS,OAAO,UAAU,UAC7B,OAAO;CAGT,OAAO,UAAU,SAAS,cAAc;AAC1C;;AAGA,MAAM,oBAAoB,UAA2B;CACnD,OAAO,yCAAyC,KAAK,KAAK;AAC5D;;AAGA,MAAM,uBAAuB,UAA2B;CACtD,OAAO,0CAA0C,KAAK,KAAK;AAC7D;;AAGA,MAAM,OAAO,UAA0B;CACrC,OAAO,OAAO,KAAK,EAAE,SAAS,GAAG,GAAG;AACtC;;AAGA,MAAM,mBACJ,mBACA,gBACkB;CAClB,MAAM,UAAU,IAAI,KAAK,iBAAiB;CAC1C,IAAI,OAAO,MAAM,QAAQ,QAAQ,CAAC,GAChC,OAAO;CAGT,MAAM,OAAO,IAAI,QAAQ,YAAY,CAAC;CACtC,IAAI,gBAAgB,QAClB,OAAO,GAAG,KAAK;CAGjB,OAAO,GAAG,KAAK,GAAG,IAAI,QAAQ,cAAc,CAAC,EAAE;AACjD;;AAGA,MAAM,uBAAuB,UAA2B;CACtD,IAAI,MAAM,SAAS,GAAG,KAAK,MAAM,SAAS,GAAG,GAC3C,OAAO;CAGT,OAAO,4BAA4B,KAAK,KAAK;AAC/C;;AAGA,MAAM,yBAAyB,UAA2B;CACxD,OAAO,sBAAsB,KAAK,KAAK,KAAK,CAAC,oBAAoB,KAAK;AACxE;;AAGA,MAAM,YAAY,WAAqC;CACrD,IAAI;EACF,OAAO,YAAY,OAAO,CAAC;CAC7B,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;;AAaA,MAAa,kBACX,OACA,aACc;CACd,IAAI,SAAS,QAAQ,UAAU,IAC7B,OAAO;CAGT,IAAI,gBAAgB,KAAK,GACvB,OAAO,YAAY,KAAK;CAG1B,IAAI,iBAAiB,MACnB,OAAO,YACL,YAAA,GAAA,wBAAA,eACkB,MAAM,YAAY,GAAG,QAAQ,KAAA,GAAA,wBAAA,sBACtB,MAAM,YAAY,CAAC,CAC9C;CAGF,IAAI,OAAO,UAAU,UACnB,OAAO;CAGT,MAAM,kBAAkB,MAAM,KAAK;CACnC,IAAI,oBAAoB,IACtB,OAAO;CAGT,IAAI,iBAAiB,eAAe,GAClC,OAAO,eAAe;EACpB,MAAM,CAAC,WAAW,aAAa,eAAe,gBAC3C,MAAM,GAAG,EACT,IAAI,MAAM;EACb,MAAM,mBAAmB,yBAAyB,QAAQ;EAK1D,QAAA,GAAA,wBAAA,oBACE,cALW,IAAI,SAKE,EAAE,GAJN,IAAI,WAIU,EAAE,GAHhB,IAAI,eAAe,CAGK,EAAE,GAAG,iBAAiB,EAC7D;CACF,CAAC;CAGH,IAAI,oBAAoB,eAAe,GACrC,OAAO,eAAe;EACpB,MAAM,mBAAmB,yBAAyB,QAAQ;EAC1D,QAAA,GAAA,wBAAA,eAAqB,cAAc,mBAAmB,gBAAgB;CACxE,CAAC;CAwBH,OACE;EArBA,GAAI,YAAY,sBAAsB,eAAe,IACjD,OACQ;GACJ,QAAA,GAAA,wBAAA,oBAA0B,GAAG,gBAAgB,GAAG,SAAS,EAAE;EAC7D,CACF,IACA,CAAC;QACC;GACJ,OAAO,YAAA,GAAA,wBAAA,eACW,iBAAiB,QAAQ,KAAA,GAAA,wBAAA,sBAClB,eAAe;EAC1C;QACM;GACJ,QAAA,GAAA,wBAAA,oBAA0B,eAAe;EAC3C;QACM;GACJ,QAAA,GAAA,wBAAA,eAAqB,eAAe;EACtC;CAIM,EACH,KAAK,WAAW;EAEf,OAAO,SAAS,MAAM;CACxB,CAAC,EACA,MAAM,gBAAgB;EACrB,OAAO,eAAe;CACxB,CAAC,KAAK;AAEZ;;AAGA,MAAa,4BAA4B,aAA8B;CACrE,OAAO,aAAA,GAAA,wBAAA,kBAA6B;AACtC;;;;;;AAOA,MAAa,gCAAgC,aAAiC;CAE5E,OAAO,aAAA,GAAA,wBAAA,oBACc,uBAFI,yBAAyB,QAES,EAAE,EAAE,CAC/D;AACF;;;;;;AAOA,MAAa,qBACX,OACA,UACA,cAAiC,aACf;CAClB,IAAI,SAAS,MACX,OAAO;CAGT,MAAM,0BAA0B;CAChC,IAAI,OAAO,wBAAwB,qBAAqB,YACtD,OAAO,gBACL,wBAAwB,iBAAiB,GACzC,WACF;CAGF,MAAM,kBAAkB;CACxB,IAAI,OAAO,gBAAgB,WAAW,YACpC,OAAO,gBACL,gBAAgB,OAAO,QAAQ,EAAE,YAAY,GAC7C,WACF;CAGF,MAAM,YAAY;CAKlB,IACE,OAAO,UAAU,SAAS,YAC1B,OAAO,UAAU,WAAW,UAK5B,OAAO,iBAAA,GAAA,wBAAA,oBAEH,cALS,IAAI,UAAU,IAKN,EAAE,GAJR,IAAI,UAAU,MAIE,EAAE,GAHlB,IAAI,UAAU,UAAU,CAGE,EAAE,GAAG,SAAS,EACrD,EAAE,iBAAiB,GACnB,WACF;CAGF,OAAO;AACT;;;ACxOA,MAAa,gBAAA,GAAA,uBAAA,IAAkB,EAC7B,OAAO;;CAEL,MAAM;;CAEN,eAAe,CAEb,KACF;;CAEA,OAAO;;CAEP,cAAc;;CAEd,cAAc;;CAEd,OAAO;;CAEP,SAAS;AACX,EACF,CAAC;;;;AAsCD,MAAM,QAAQ,EACZ,WAAW,aAAa,KAAA,GACxB,cAAc,UACd,eAAe,OACf,eAAe,OACf,YAAY,KAAA,GACZ,MACA,WAAW,KAAA,GACX,YAAY,UACZ,GAAG,wBACY;CACf,MAAM,EACJ,WACA,UACA,cACA,OAAO,EAAE,QAAQ,UAAU,SAC3B,SACA,OACA,UACA,WACEA,oCAAAA,gBAAgB;EAAE;EAAM,GAAG;CAAkB,CAAC;CAIlD,MAAM,cAAA,GAAA,uBAAA,sBADW,aAC8B,GAAG,YAAY,MAAM;CACpE,MAAM,mBAAmB,yBAAyB,QAAQ;CAC1D,MAAM,uBAAuB,eAAe,SAAS;CASrD,MAAM,cAAc,eALlB,gBAAgB,OAAO,UAAU,WAC7B,GAAG,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,EAAE,SAAS,GAAG,GAAG,EAAE,QAC7D,OAGmD,QAAQ;CACjE,MAAM,mBAAmB,6BAA6B,QAAQ;CAG9D,MAAM,gBAAgB,cAAyB;EAC7C,IAAI,aAAa,MAAM;GACrB,SAAS,IAAI;GACb;EACF;EAEA,IAAI,cAAc;GAChB,MAAM,kBAAkB,kBACtB,WACA,kBACA,MACF;GAEA,IAAI,CAAC,iBAAiB;IACpB,SAAS,IAAI;IACb;GACF;GAEA,MAAM,CAAC,gBAAgB,gBAAgB,MAAM,GAAG;GAChD,MAAM,cAAc,OAAO,YAAY;GACvC,SAAS,OAAO,MAAM,WAAW,IAAI,OAAO,WAAW;GACvD;EACF;EAEA,IAAI,cAAc,aAAa;GAC7B,SAAS,SAAS;GAClB;EACF;EAEA,SACE,kBAAkB,WAAW,kBAAkB,oBAAoB,KACjE,OAAO,SAAS,CACpB;CACF;CAEA,OACE,iBAAA,GAAA,kBAAA,KAACC,mBAAAA,WAAD;EACE,cAAY,QAAQ,KAAA,IAAY;EAChC,YAAY;GACV,MAAM,WAAW;GACjB,eAAe,WAAW;GAC1B,cAAc,WAAW;GACzB,OAAO,WAAW;GAClB,cAAc,WAAW;GACzB,OAAO,WAAW;GAClB,SAAS,WAAW;EACtB;EACA,eAAa;EACC;EACd,aAAa;EACC;EACH;EACX,IAAI;EACJ,YAAY;EACZ,WAAW;EACX,YAAY;EACL;EACP,gBAAe;EACT;EACE;EACR,UAAU;EACV,kBAAkB,oBAAoB,KAAA;EACtC,QAAO;EACP,OAAO,eAAe,KAAA;EACtB,SAAQ;CACT,CAAA;AAEL;;;AC5KA,IAAA,eAAe"}
|
|
@@ -56,17 +56,17 @@ const useUniformFieldArray = ({ name, flat = false, elementInitialValue: _elemen
|
|
|
56
56
|
return flat ? { [require_helpers_index.flatArrayKey]: _elementInitialValue ?? null } : _elementInitialValue ?? {};
|
|
57
57
|
}, [flat, _elementInitialValue]);
|
|
58
58
|
require_hooks_useWatchFormReset_index.useWatchFormReset({ onReset: () => {
|
|
59
|
-
if (!lastElementNotRemovable) return;
|
|
60
59
|
const currentValue = getValues(name);
|
|
61
60
|
const valueIsArray = Array.isArray(currentValue);
|
|
62
61
|
const arrayValue = valueIsArray ? currentValue : [];
|
|
63
|
-
const
|
|
62
|
+
const normalizedLength = lastElementNotRemovable ? 1 : 0;
|
|
63
|
+
const alreadyNormalized = valueIsArray && arrayValue.length === normalizedLength;
|
|
64
64
|
if (!(!valueIsArray || arrayValue.length === 0 || arrayValue.every((entry) => {
|
|
65
65
|
return require_helpers_index.isValueEmpty(entry);
|
|
66
66
|
}))) return;
|
|
67
|
-
if (
|
|
67
|
+
if (alreadyNormalized) return;
|
|
68
68
|
setDisableAnimation(true);
|
|
69
|
-
replace([elementInitialValue]);
|
|
69
|
+
replace(lastElementNotRemovable ? [elementInitialValue] : []);
|
|
70
70
|
if (!prefersReducedMotion) setTimeout(() => {
|
|
71
71
|
setDisableAnimation(false);
|
|
72
72
|
}, 1);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["useUniformField","useFormContext","flatArrayKey","isValueEmpty"],"sources":["../../../src/hooks/useUniformFieldArray/useUniformFieldArray.ts","../../../src/hooks/useUniformFieldArray/index.ts"],"sourcesContent":["import type { ReactNode } from 'react';\nimport type { ArrayPath, FieldValues, Path } from 'react-hook-form';\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { useFieldArray as useRHFFieldArray } from 'react-hook-form';\n\nimport { useReducedMotion } from '@fuf-stack/pixel-motion';\n\nimport { flatArrayKey, isValueEmpty } from '../../helpers';\nimport { useFormContext } from '../useFormContext';\nimport { useUniformField } from '../useUniformField';\nimport { useWatchFormReset } from '../useWatchFormReset';\n\nexport interface UseUniformFieldArrayProps<\n TFieldValues extends FieldValues = FieldValues,\n> {\n /** Field name for the array */\n name: ArrayPath<TFieldValues>;\n /** Whether this is a flat array (array of primitives) */\n flat?: boolean;\n /** Initial value for new array elements */\n elementInitialValue?: unknown;\n /** Whether the last element cannot be removed (always maintain at least one element) */\n lastElementNotRemovable?: boolean;\n /** Disable the field */\n disabled?: boolean;\n /** Optional explicit test id used to build stable test ids */\n testId?: string;\n /** Optional label content */\n label?: ReactNode;\n}\n\n/**\n * Enhanced useFieldArray hook with initialization and animation logic.\n * Based on React Hook Form's useFieldArray with additional features:\n * - Automatic initialization when lastElementNotRemovable is set\n * - Reset-only normalization for stale empty placeholder rows (via useWatchFormReset)\n * - Animation control (disabled during initialization)\n * - Temporary animation disable during reset normalization collapse\n * - Support for flat arrays (arrays of primitives)\n *\n * Note: Automatic validation triggering on length change is disabled to prevent\n * triggering form-wide validation. Array validation still runs on form submission.\n *\n * @see https://react-hook-form.com/docs/usefieldarray\n */\nexport const useUniformFieldArray = <\n TFieldValues extends FieldValues = FieldValues,\n>({\n name,\n flat = false,\n elementInitialValue: _elementInitialValue = null,\n lastElementNotRemovable = false,\n disabled,\n testId: explicitTestId,\n label,\n}: UseUniformFieldArrayProps<TFieldValues>) => {\n // Get uniform field state and utilities\n const uniformField = useUniformField<TFieldValues>({\n name: name as Path<TFieldValues> & string,\n disabled,\n testId: explicitTestId,\n label,\n });\n\n const { control } = uniformField;\n\n const { fields, append, remove, insert, move, replace } = useRHFFieldArray({\n control,\n name,\n });\n\n const { trigger, setValue, getValues } = useFormContext<TFieldValues>();\n\n // Determine if initialization is needed (initially or after reset).\n // lastElementNotRemovable is purely a minimum-count guarantee: when there are\n // no rows, add one. This handles both:\n // - Initial mount: fields.length starts at 0\n // - Form reset to an empty array: fields.length becomes 0 again\n // It intentionally does NOT inspect row contents, so manually added empty rows\n // are never collapsed.\n const needsInitialize = useMemo(() => {\n return lastElementNotRemovable && fields.length === 0;\n }, [lastElementNotRemovable, fields.length]);\n\n // Track whether initialization has completed. Initialized contextually:\n // - If initialization IS needed (needsInitialize = true): starts as false, set to true after init\n // - If initialization is NOT needed (needsInitialize = false): starts as true (already initialized)\n // This ref is used to:\n // 1. Skip validation during initialization/re-initialization\n // 2. Gate animation enabling until after initialization\n // 3. Gate motion preference effect until after initialization\n const hasInitialized = useRef(!needsInitialize);\n\n // Reset initialization flag when needsInitialize changes to true.\n // This handles form reset: when fields become empty (needsInitialize becomes true),\n // hasInitialized is reset to false, triggering re-initialization in the effect below.\n useEffect(() => {\n if (needsInitialize) {\n hasInitialized.current = false;\n }\n }, [needsInitialize]);\n\n // Validate array-level constraints (min/max items) when length changes.\n // This ensures min/max errors appear instantly when user adds/removes items.\n // Note: Child field validation also runs, but new empty fields won't show as invalid\n // because useFormContext only sets invalid=true for touched fields or after form submission.\n // Skip validation during initialization/re-initialization to avoid showing errors prematurely.\n useEffect(() => {\n if (hasInitialized.current) {\n setTimeout(() => {\n trigger(name as Path<TFieldValues>);\n }, 200);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [fields.length]);\n\n // Animation control: Start with animations disabled to prevent animating in initial elements.\n // Will be enabled after initialization completes (unless user prefers reduced motion).\n const [disableAnimation, setDisableAnimation] = useState(true);\n\n // Respond to user's motion preference changes (after initialization).\n // During initialization, animations stay disabled regardless of preference.\n const prefersReducedMotion = useReducedMotion();\n useEffect(() => {\n if (hasInitialized.current) {\n setDisableAnimation(!!prefersReducedMotion);\n }\n }, [prefersReducedMotion]);\n\n // Prepare initial element value based on mode\n // - flat=true: arrays of primitives → object with flatArrayKey and null value by default\n // - flat=false: arrays of objects → empty object by default\n const elementInitialValue = useMemo(() => {\n return flat\n ? { [flatArrayKey]: _elementInitialValue ?? null }\n : (_elementInitialValue ?? {});\n }, [flat, _elementInitialValue]);\n\n // Reset normalization:\n // Run ONLY when an actual form reset is emitted (via useWatchFormReset), not\n // on regular field edits. This does not collapse rows users add manually.\n //\n // Why this exists:\n // RHF can keep stale field-array rows after reset when array defaults are\n // missing (e.g. value becomes undefined or [null, null] while UI still has\n // multiple rows). For lastElementNotRemovable arrays we normalize that state\n // back to exactly one empty row.\n useWatchFormReset({\n onReset: () => {\n // This normalization is only relevant for min-one arrays.\n if (!lastElementNotRemovable) {\n return;\n }\n\n const currentValue = getValues(name as Path<TFieldValues>) as unknown;\n const valueIsArray = Array.isArray(currentValue);\n const arrayValue = valueIsArray ? (currentValue as unknown[]) : [];\n const hasSingleRow = valueIsArray && arrayValue.length === 1;\n\n // Treat these as \"effectively empty after reset\":\n // - value missing/not-array\n // - empty array\n // - array where all entries are empty placeholders\n const isEffectivelyEmptyAfterReset =\n !valueIsArray ||\n arrayValue.length === 0 ||\n arrayValue.every((entry) => {\n return isValueEmpty(entry);\n });\n\n // Nothing to fix when the reset restored real values (e.g. from defaults).\n if (!isEffectivelyEmptyAfterReset) {\n return;\n }\n\n // Already normalized: exactly one row remains.\n if (hasSingleRow) {\n return;\n }\n\n // Avoid collapse animation flicker during reset normalization.\n setDisableAnimation(true);\n\n // use replace so the RHF field-array length actually collapses to one row\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n replace([elementInitialValue] as any);\n\n // Restore normal animation state right after normalization.\n if (!prefersReducedMotion) {\n setTimeout(() => {\n setDisableAnimation(false);\n }, 1);\n }\n },\n });\n\n // Initialization/Re-initialization: add one element only when array length is 0.\n // This is the min-count behavior for lastElementNotRemovable and is intentionally\n // separate from reset normalization above.\n //\n // Reset behavior in this hook is split into two phases:\n // 1) Reset normalization (useWatchFormReset): collapse stale placeholder rows\n // left by reset edge cases.\n // 2) Length-based initialization (this effect): ensure a min-one array when\n // the field array is truly empty.\n // CRITICAL: This effect MUST be the LAST hook in this component.\n // It sets hasInitialized.current = true, which acts as a gate for other effects.\n // If this runs before other effects, hasInitialized will be true during their first run,\n // causing them to execute logic meant only for post-initialization (e.g., validation,\n // animation enabling). By placing this last, all other effects run first with\n // hasInitialized = false, allowing them to skip initialization-phase logic.\n useEffect(\n () => {\n if (needsInitialize) {\n // use setValue instead of append to avoid focusing the added element\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n setValue(name as Path<TFieldValues>, [elementInitialValue] as any, {\n shouldDirty: false,\n shouldTouch: false,\n });\n\n // Mark initialization as complete\n hasInitialized.current = true;\n\n // Enable animations after a brief delay (unless user prefers reduced motion or animations are already enabled).\n // This only runs on initial mount when animations start disabled.\n // On reset, disableAnimation is typically false, so this setTimeout won't run and animations stay enabled.\n if (!prefersReducedMotion && disableAnimation) {\n setTimeout(() => {\n setDisableAnimation(false);\n }, 1);\n }\n }\n },\n // Run when needsInitialize changes (initial mount or reset)\n // needsInitialize is memoized based on fields.length and lastElementNotRemovable\n // Other dependencies are intentionally omitted:\n // - append, setValue, trigger, setDisableAnimation are stable refs/functions\n // - elementInitialValue, name, flat, prefersReducedMotion, disableAnimation are props/stable values\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [needsInitialize],\n );\n\n return {\n // Field array methods and state\n fields,\n append,\n remove,\n insert,\n move,\n disableAnimation,\n elementInitialValue,\n // Uniform field state and utilities (spread all)\n ...uniformField,\n };\n};\n","/* v8 ignore start */\n\nexport * from './useUniformFieldArray';\n\n/* v8 ignore stop */\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA8CA,MAAa,wBAEX,EACA,MACA,OAAO,OACP,qBAAqB,uBAAuB,MAC5C,0BAA0B,OAC1B,UACA,QAAQ,gBACR,YAC6C;CAE7C,MAAM,eAAeA,oCAAAA,gBAA8B;EAC3C;EACN;EACA,QAAQ;EACR;CACF,CAAC;CAED,MAAM,EAAE,YAAY;CAEpB,MAAM,EAAE,QAAQ,QAAQ,QAAQ,QAAQ,MAAM,aAAA,GAAA,gBAAA,eAA6B;EACzE;EACA;CACF,CAAC;CAED,MAAM,EAAE,SAAS,UAAU,cAAcC,uBAAAA,eAA6B;CAStE,MAAM,mBAAA,GAAA,MAAA,eAAgC;EACpC,OAAO,2BAA2B,OAAO,WAAW;CACtD,GAAG,CAAC,yBAAyB,OAAO,MAAM,CAAC;CAS3C,MAAM,kBAAA,GAAA,MAAA,QAAwB,CAAC,eAAe;CAK9C,CAAA,GAAA,MAAA,iBAAgB;EACd,IAAI,iBACF,eAAe,UAAU;CAE7B,GAAG,CAAC,eAAe,CAAC;CAOpB,CAAA,GAAA,MAAA,iBAAgB;EACd,IAAI,eAAe,SACjB,iBAAiB;GACf,QAAQ,IAA0B;EACpC,GAAG,GAAG;CAGV,GAAG,CAAC,OAAO,MAAM,CAAC;CAIlB,MAAM,CAAC,kBAAkB,wBAAA,GAAA,MAAA,UAAgC,IAAI;CAI7D,MAAM,wBAAA,GAAA,wBAAA,kBAAwC;CAC9C,CAAA,GAAA,MAAA,iBAAgB;EACd,IAAI,eAAe,SACjB,oBAAoB,CAAC,CAAC,oBAAoB;CAE9C,GAAG,CAAC,oBAAoB,CAAC;CAKzB,MAAM,uBAAA,GAAA,MAAA,eAAoC;EACxC,OAAO,OACH,GAAGC,sBAAAA,eAAe,wBAAwB,KAAK,IAC9C,wBAAwB,CAAC;CAChC,GAAG,CAAC,MAAM,oBAAoB,CAAC;CAW/B,sCAAA,kBAAkB,EAChB,eAAe;EAEb,IAAI,CAAC,yBACH;EAGF,MAAM,eAAe,UAAU,IAA0B;EACzD,MAAM,eAAe,MAAM,QAAQ,YAAY;EAC/C,MAAM,aAAa,eAAgB,eAA6B,CAAC;EACjE,MAAM,eAAe,gBAAgB,WAAW,WAAW;EAc3D,IAAI,EAPF,CAAC,gBACD,WAAW,WAAW,KACtB,WAAW,OAAO,UAAU;GAC1B,OAAOC,sBAAAA,aAAa,KAAK;EAC3B,CAAC,IAID;EAIF,IAAI,cACF;EAIF,oBAAoB,IAAI;EAIxB,QAAQ,CAAC,mBAAmB,CAAQ;EAGpC,IAAI,CAAC,sBACH,iBAAiB;GACf,oBAAoB,KAAK;EAC3B,GAAG,CAAC;CAER,EACF,CAAC;CAiBD,CAAA,GAAA,MAAA,iBACQ;EACJ,IAAI,iBAAiB;GAGnB,SAAS,MAA4B,CAAC,mBAAmB,GAAU;IACjE,aAAa;IACb,aAAa;GACf,CAAC;GAGD,eAAe,UAAU;GAKzB,IAAI,CAAC,wBAAwB,kBAC3B,iBAAiB;IACf,oBAAoB,KAAK;GAC3B,GAAG,CAAC;EAER;CACF,GAOA,CAAC,eAAe,CAClB;CAEA,OAAO;EAEL;EACA;EACA;EACA;EACA;EACA;EACA;EAEA,GAAG;CACL;AACF"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["useUniformField","useFormContext","flatArrayKey","isValueEmpty"],"sources":["../../../src/hooks/useUniformFieldArray/useUniformFieldArray.ts","../../../src/hooks/useUniformFieldArray/index.ts"],"sourcesContent":["import type { ReactNode } from 'react';\nimport type { ArrayPath, FieldValues, Path } from 'react-hook-form';\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { useFieldArray as useRHFFieldArray } from 'react-hook-form';\n\nimport { useReducedMotion } from '@fuf-stack/pixel-motion';\n\nimport { flatArrayKey, isValueEmpty } from '../../helpers';\nimport { useFormContext } from '../useFormContext';\nimport { useUniformField } from '../useUniformField';\nimport { useWatchFormReset } from '../useWatchFormReset';\n\nexport interface UseUniformFieldArrayProps<\n TFieldValues extends FieldValues = FieldValues,\n> {\n /** Field name for the array */\n name: ArrayPath<TFieldValues>;\n /** Whether this is a flat array (array of primitives) */\n flat?: boolean;\n /** Initial value for new array elements */\n elementInitialValue?: unknown;\n /** Whether the last element cannot be removed (always maintain at least one element) */\n lastElementNotRemovable?: boolean;\n /** Disable the field */\n disabled?: boolean;\n /** Optional explicit test id used to build stable test ids */\n testId?: string;\n /** Optional label content */\n label?: ReactNode;\n}\n\n/**\n * Enhanced useFieldArray hook with initialization and animation logic.\n * Based on React Hook Form's useFieldArray with additional features:\n * - Automatic initialization when lastElementNotRemovable is set\n * - Reset-only normalization for stale empty placeholder rows (via useWatchFormReset)\n * - Animation control (disabled during initialization)\n * - Temporary animation disable during reset normalization collapse\n * - Support for flat arrays (arrays of primitives)\n *\n * Note: Automatic validation triggering on length change is disabled to prevent\n * triggering form-wide validation. Array validation still runs on form submission.\n *\n * @see https://react-hook-form.com/docs/usefieldarray\n */\nexport const useUniformFieldArray = <\n TFieldValues extends FieldValues = FieldValues,\n>({\n name,\n flat = false,\n elementInitialValue: _elementInitialValue = null,\n lastElementNotRemovable = false,\n disabled,\n testId: explicitTestId,\n label,\n}: UseUniformFieldArrayProps<TFieldValues>) => {\n // Get uniform field state and utilities\n const uniformField = useUniformField<TFieldValues>({\n name: name as Path<TFieldValues> & string,\n disabled,\n testId: explicitTestId,\n label,\n });\n\n const { control } = uniformField;\n\n const { fields, append, remove, insert, move, replace } = useRHFFieldArray({\n control,\n name,\n });\n\n const { trigger, setValue, getValues } = useFormContext<TFieldValues>();\n\n // Determine if initialization is needed (initially or after reset).\n // lastElementNotRemovable is purely a minimum-count guarantee: when there are\n // no rows, add one. This handles both:\n // - Initial mount: fields.length starts at 0\n // - Form reset to an empty array: fields.length becomes 0 again\n // It intentionally does NOT inspect row contents, so manually added empty rows\n // are never collapsed.\n const needsInitialize = useMemo(() => {\n return lastElementNotRemovable && fields.length === 0;\n }, [lastElementNotRemovable, fields.length]);\n\n // Track whether initialization has completed. Initialized contextually:\n // - If initialization IS needed (needsInitialize = true): starts as false, set to true after init\n // - If initialization is NOT needed (needsInitialize = false): starts as true (already initialized)\n // This ref is used to:\n // 1. Skip validation during initialization/re-initialization\n // 2. Gate animation enabling until after initialization\n // 3. Gate motion preference effect until after initialization\n const hasInitialized = useRef(!needsInitialize);\n\n // Reset initialization flag when needsInitialize changes to true.\n // This handles form reset: when fields become empty (needsInitialize becomes true),\n // hasInitialized is reset to false, triggering re-initialization in the effect below.\n useEffect(() => {\n if (needsInitialize) {\n hasInitialized.current = false;\n }\n }, [needsInitialize]);\n\n // Validate array-level constraints (min/max items) when length changes.\n // This ensures min/max errors appear instantly when user adds/removes items.\n // Note: Child field validation also runs, but new empty fields won't show as invalid\n // because useFormContext only sets invalid=true for touched fields or after form submission.\n // Skip validation during initialization/re-initialization to avoid showing errors prematurely.\n useEffect(() => {\n if (hasInitialized.current) {\n setTimeout(() => {\n trigger(name as Path<TFieldValues>);\n }, 200);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [fields.length]);\n\n // Animation control: Start with animations disabled to prevent animating in initial elements.\n // Will be enabled after initialization completes (unless user prefers reduced motion).\n const [disableAnimation, setDisableAnimation] = useState(true);\n\n // Respond to user's motion preference changes (after initialization).\n // During initialization, animations stay disabled regardless of preference.\n const prefersReducedMotion = useReducedMotion();\n useEffect(() => {\n if (hasInitialized.current) {\n setDisableAnimation(!!prefersReducedMotion);\n }\n }, [prefersReducedMotion]);\n\n // Prepare initial element value based on mode\n // - flat=true: arrays of primitives → object with flatArrayKey and null value by default\n // - flat=false: arrays of objects → empty object by default\n const elementInitialValue = useMemo(() => {\n return flat\n ? { [flatArrayKey]: _elementInitialValue ?? null }\n : (_elementInitialValue ?? {});\n }, [flat, _elementInitialValue]);\n\n // Reset normalization:\n // Run ONLY when an actual form reset is emitted (via useWatchFormReset), not\n // on regular field edits. This does not collapse rows users add manually.\n //\n // Why this exists:\n // RHF can keep stale field-array rows after reset when array defaults are\n // missing (e.g. value becomes undefined or [null, null] while UI still has\n // multiple rows). We normalize this reset-only state to:\n // - one row when lastElementNotRemovable is enabled\n // - zero rows otherwise\n useWatchFormReset({\n onReset: () => {\n const currentValue = getValues(name as Path<TFieldValues>) as unknown;\n const valueIsArray = Array.isArray(currentValue);\n const arrayValue = valueIsArray ? (currentValue as unknown[]) : [];\n const normalizedLength = lastElementNotRemovable ? 1 : 0;\n const alreadyNormalized =\n valueIsArray && arrayValue.length === normalizedLength;\n\n // Treat these as \"effectively empty after reset\":\n // - value missing/not-array\n // - empty array\n // - array where all entries are empty placeholders\n const isEffectivelyEmptyAfterReset =\n !valueIsArray ||\n arrayValue.length === 0 ||\n arrayValue.every((entry) => {\n return isValueEmpty(entry);\n });\n\n // Nothing to fix when the reset restored real values (e.g. from defaults).\n if (!isEffectivelyEmptyAfterReset) {\n return;\n }\n\n // Already normalized to the target row count.\n if (alreadyNormalized) {\n return;\n }\n\n // Avoid collapse animation flicker during reset normalization.\n setDisableAnimation(true);\n\n // use replace so the RHF field-array length actually matches the target.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n replace((lastElementNotRemovable ? [elementInitialValue] : []) as any);\n\n // Restore normal animation state right after normalization.\n if (!prefersReducedMotion) {\n setTimeout(() => {\n setDisableAnimation(false);\n }, 1);\n }\n },\n });\n\n // Initialization/Re-initialization: add one element only when array length is 0.\n // This is the min-count behavior for lastElementNotRemovable and is intentionally\n // separate from reset normalization above.\n //\n // Reset behavior in this hook is split into two phases:\n // 1) Reset normalization (useWatchFormReset): collapse stale placeholder rows\n // left by reset edge cases.\n // 2) Length-based initialization (this effect): ensure a min-one array when\n // the field array is truly empty.\n // CRITICAL: This effect MUST be the LAST hook in this component.\n // It sets hasInitialized.current = true, which acts as a gate for other effects.\n // If this runs before other effects, hasInitialized will be true during their first run,\n // causing them to execute logic meant only for post-initialization (e.g., validation,\n // animation enabling). By placing this last, all other effects run first with\n // hasInitialized = false, allowing them to skip initialization-phase logic.\n useEffect(\n () => {\n if (needsInitialize) {\n // use setValue instead of append to avoid focusing the added element\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n setValue(name as Path<TFieldValues>, [elementInitialValue] as any, {\n shouldDirty: false,\n shouldTouch: false,\n });\n\n // Mark initialization as complete\n hasInitialized.current = true;\n\n // Enable animations after a brief delay (unless user prefers reduced motion or animations are already enabled).\n // This only runs on initial mount when animations start disabled.\n // On reset, disableAnimation is typically false, so this setTimeout won't run and animations stay enabled.\n if (!prefersReducedMotion && disableAnimation) {\n setTimeout(() => {\n setDisableAnimation(false);\n }, 1);\n }\n }\n },\n // Run when needsInitialize changes (initial mount or reset)\n // needsInitialize is memoized based on fields.length and lastElementNotRemovable\n // Other dependencies are intentionally omitted:\n // - append, setValue, trigger, setDisableAnimation are stable refs/functions\n // - elementInitialValue, name, flat, prefersReducedMotion, disableAnimation are props/stable values\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [needsInitialize],\n );\n\n return {\n // Field array methods and state\n fields,\n append,\n remove,\n insert,\n move,\n disableAnimation,\n elementInitialValue,\n // Uniform field state and utilities (spread all)\n ...uniformField,\n };\n};\n","/* v8 ignore start */\n\nexport * from './useUniformFieldArray';\n\n/* v8 ignore stop */\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA8CA,MAAa,wBAEX,EACA,MACA,OAAO,OACP,qBAAqB,uBAAuB,MAC5C,0BAA0B,OAC1B,UACA,QAAQ,gBACR,YAC6C;CAE7C,MAAM,eAAeA,oCAAAA,gBAA8B;EAC3C;EACN;EACA,QAAQ;EACR;CACF,CAAC;CAED,MAAM,EAAE,YAAY;CAEpB,MAAM,EAAE,QAAQ,QAAQ,QAAQ,QAAQ,MAAM,aAAA,GAAA,gBAAA,eAA6B;EACzE;EACA;CACF,CAAC;CAED,MAAM,EAAE,SAAS,UAAU,cAAcC,uBAAAA,eAA6B;CAStE,MAAM,mBAAA,GAAA,MAAA,eAAgC;EACpC,OAAO,2BAA2B,OAAO,WAAW;CACtD,GAAG,CAAC,yBAAyB,OAAO,MAAM,CAAC;CAS3C,MAAM,kBAAA,GAAA,MAAA,QAAwB,CAAC,eAAe;CAK9C,CAAA,GAAA,MAAA,iBAAgB;EACd,IAAI,iBACF,eAAe,UAAU;CAE7B,GAAG,CAAC,eAAe,CAAC;CAOpB,CAAA,GAAA,MAAA,iBAAgB;EACd,IAAI,eAAe,SACjB,iBAAiB;GACf,QAAQ,IAA0B;EACpC,GAAG,GAAG;CAGV,GAAG,CAAC,OAAO,MAAM,CAAC;CAIlB,MAAM,CAAC,kBAAkB,wBAAA,GAAA,MAAA,UAAgC,IAAI;CAI7D,MAAM,wBAAA,GAAA,wBAAA,kBAAwC;CAC9C,CAAA,GAAA,MAAA,iBAAgB;EACd,IAAI,eAAe,SACjB,oBAAoB,CAAC,CAAC,oBAAoB;CAE9C,GAAG,CAAC,oBAAoB,CAAC;CAKzB,MAAM,uBAAA,GAAA,MAAA,eAAoC;EACxC,OAAO,OACH,GAAGC,sBAAAA,eAAe,wBAAwB,KAAK,IAC9C,wBAAwB,CAAC;CAChC,GAAG,CAAC,MAAM,oBAAoB,CAAC;CAY/B,sCAAA,kBAAkB,EAChB,eAAe;EACb,MAAM,eAAe,UAAU,IAA0B;EACzD,MAAM,eAAe,MAAM,QAAQ,YAAY;EAC/C,MAAM,aAAa,eAAgB,eAA6B,CAAC;EACjE,MAAM,mBAAmB,0BAA0B,IAAI;EACvD,MAAM,oBACJ,gBAAgB,WAAW,WAAW;EAcxC,IAAI,EAPF,CAAC,gBACD,WAAW,WAAW,KACtB,WAAW,OAAO,UAAU;GAC1B,OAAOC,sBAAAA,aAAa,KAAK;EAC3B,CAAC,IAID;EAIF,IAAI,mBACF;EAIF,oBAAoB,IAAI;EAIxB,QAAS,0BAA0B,CAAC,mBAAmB,IAAI,CAAC,CAAS;EAGrE,IAAI,CAAC,sBACH,iBAAiB;GACf,oBAAoB,KAAK;EAC3B,GAAG,CAAC;CAER,EACF,CAAC;CAiBD,CAAA,GAAA,MAAA,iBACQ;EACJ,IAAI,iBAAiB;GAGnB,SAAS,MAA4B,CAAC,mBAAmB,GAAU;IACjE,aAAa;IACb,aAAa;GACf,CAAC;GAGD,eAAe,UAAU;GAKzB,IAAI,CAAC,wBAAwB,kBAC3B,iBAAiB;IACf,oBAAoB,KAAK;GAC3B,GAAG,CAAC;EAER;CACF,GAOA,CAAC,eAAe,CAClB;CAEA,OAAO;EAEL;EACA;EACA;EACA;EACA;EACA;EACA;EAEA,GAAG;CACL;AACF"}
|
|
@@ -54,17 +54,17 @@ const useUniformFieldArray = ({ name, flat = false, elementInitialValue: _elemen
|
|
|
54
54
|
return flat ? { [flatArrayKey]: _elementInitialValue ?? null } : _elementInitialValue ?? {};
|
|
55
55
|
}, [flat, _elementInitialValue]);
|
|
56
56
|
useWatchFormReset({ onReset: () => {
|
|
57
|
-
if (!lastElementNotRemovable) return;
|
|
58
57
|
const currentValue = getValues(name);
|
|
59
58
|
const valueIsArray = Array.isArray(currentValue);
|
|
60
59
|
const arrayValue = valueIsArray ? currentValue : [];
|
|
61
|
-
const
|
|
60
|
+
const normalizedLength = lastElementNotRemovable ? 1 : 0;
|
|
61
|
+
const alreadyNormalized = valueIsArray && arrayValue.length === normalizedLength;
|
|
62
62
|
if (!(!valueIsArray || arrayValue.length === 0 || arrayValue.every((entry) => {
|
|
63
63
|
return isValueEmpty(entry);
|
|
64
64
|
}))) return;
|
|
65
|
-
if (
|
|
65
|
+
if (alreadyNormalized) return;
|
|
66
66
|
setDisableAnimation(true);
|
|
67
|
-
replace([elementInitialValue]);
|
|
67
|
+
replace(lastElementNotRemovable ? [elementInitialValue] : []);
|
|
68
68
|
if (!prefersReducedMotion) setTimeout(() => {
|
|
69
69
|
setDisableAnimation(false);
|
|
70
70
|
}, 1);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["useRHFFieldArray","useFormContext"],"sources":["../../../src/hooks/useUniformFieldArray/useUniformFieldArray.ts","../../../src/hooks/useUniformFieldArray/index.ts"],"sourcesContent":["import type { ReactNode } from 'react';\nimport type { ArrayPath, FieldValues, Path } from 'react-hook-form';\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { useFieldArray as useRHFFieldArray } from 'react-hook-form';\n\nimport { useReducedMotion } from '@fuf-stack/pixel-motion';\n\nimport { flatArrayKey, isValueEmpty } from '../../helpers';\nimport { useFormContext } from '../useFormContext';\nimport { useUniformField } from '../useUniformField';\nimport { useWatchFormReset } from '../useWatchFormReset';\n\nexport interface UseUniformFieldArrayProps<\n TFieldValues extends FieldValues = FieldValues,\n> {\n /** Field name for the array */\n name: ArrayPath<TFieldValues>;\n /** Whether this is a flat array (array of primitives) */\n flat?: boolean;\n /** Initial value for new array elements */\n elementInitialValue?: unknown;\n /** Whether the last element cannot be removed (always maintain at least one element) */\n lastElementNotRemovable?: boolean;\n /** Disable the field */\n disabled?: boolean;\n /** Optional explicit test id used to build stable test ids */\n testId?: string;\n /** Optional label content */\n label?: ReactNode;\n}\n\n/**\n * Enhanced useFieldArray hook with initialization and animation logic.\n * Based on React Hook Form's useFieldArray with additional features:\n * - Automatic initialization when lastElementNotRemovable is set\n * - Reset-only normalization for stale empty placeholder rows (via useWatchFormReset)\n * - Animation control (disabled during initialization)\n * - Temporary animation disable during reset normalization collapse\n * - Support for flat arrays (arrays of primitives)\n *\n * Note: Automatic validation triggering on length change is disabled to prevent\n * triggering form-wide validation. Array validation still runs on form submission.\n *\n * @see https://react-hook-form.com/docs/usefieldarray\n */\nexport const useUniformFieldArray = <\n TFieldValues extends FieldValues = FieldValues,\n>({\n name,\n flat = false,\n elementInitialValue: _elementInitialValue = null,\n lastElementNotRemovable = false,\n disabled,\n testId: explicitTestId,\n label,\n}: UseUniformFieldArrayProps<TFieldValues>) => {\n // Get uniform field state and utilities\n const uniformField = useUniformField<TFieldValues>({\n name: name as Path<TFieldValues> & string,\n disabled,\n testId: explicitTestId,\n label,\n });\n\n const { control } = uniformField;\n\n const { fields, append, remove, insert, move, replace } = useRHFFieldArray({\n control,\n name,\n });\n\n const { trigger, setValue, getValues } = useFormContext<TFieldValues>();\n\n // Determine if initialization is needed (initially or after reset).\n // lastElementNotRemovable is purely a minimum-count guarantee: when there are\n // no rows, add one. This handles both:\n // - Initial mount: fields.length starts at 0\n // - Form reset to an empty array: fields.length becomes 0 again\n // It intentionally does NOT inspect row contents, so manually added empty rows\n // are never collapsed.\n const needsInitialize = useMemo(() => {\n return lastElementNotRemovable && fields.length === 0;\n }, [lastElementNotRemovable, fields.length]);\n\n // Track whether initialization has completed. Initialized contextually:\n // - If initialization IS needed (needsInitialize = true): starts as false, set to true after init\n // - If initialization is NOT needed (needsInitialize = false): starts as true (already initialized)\n // This ref is used to:\n // 1. Skip validation during initialization/re-initialization\n // 2. Gate animation enabling until after initialization\n // 3. Gate motion preference effect until after initialization\n const hasInitialized = useRef(!needsInitialize);\n\n // Reset initialization flag when needsInitialize changes to true.\n // This handles form reset: when fields become empty (needsInitialize becomes true),\n // hasInitialized is reset to false, triggering re-initialization in the effect below.\n useEffect(() => {\n if (needsInitialize) {\n hasInitialized.current = false;\n }\n }, [needsInitialize]);\n\n // Validate array-level constraints (min/max items) when length changes.\n // This ensures min/max errors appear instantly when user adds/removes items.\n // Note: Child field validation also runs, but new empty fields won't show as invalid\n // because useFormContext only sets invalid=true for touched fields or after form submission.\n // Skip validation during initialization/re-initialization to avoid showing errors prematurely.\n useEffect(() => {\n if (hasInitialized.current) {\n setTimeout(() => {\n trigger(name as Path<TFieldValues>);\n }, 200);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [fields.length]);\n\n // Animation control: Start with animations disabled to prevent animating in initial elements.\n // Will be enabled after initialization completes (unless user prefers reduced motion).\n const [disableAnimation, setDisableAnimation] = useState(true);\n\n // Respond to user's motion preference changes (after initialization).\n // During initialization, animations stay disabled regardless of preference.\n const prefersReducedMotion = useReducedMotion();\n useEffect(() => {\n if (hasInitialized.current) {\n setDisableAnimation(!!prefersReducedMotion);\n }\n }, [prefersReducedMotion]);\n\n // Prepare initial element value based on mode\n // - flat=true: arrays of primitives → object with flatArrayKey and null value by default\n // - flat=false: arrays of objects → empty object by default\n const elementInitialValue = useMemo(() => {\n return flat\n ? { [flatArrayKey]: _elementInitialValue ?? null }\n : (_elementInitialValue ?? {});\n }, [flat, _elementInitialValue]);\n\n // Reset normalization:\n // Run ONLY when an actual form reset is emitted (via useWatchFormReset), not\n // on regular field edits. This does not collapse rows users add manually.\n //\n // Why this exists:\n // RHF can keep stale field-array rows after reset when array defaults are\n // missing (e.g. value becomes undefined or [null, null] while UI still has\n // multiple rows). For lastElementNotRemovable arrays we normalize that state\n // back to exactly one empty row.\n useWatchFormReset({\n onReset: () => {\n // This normalization is only relevant for min-one arrays.\n if (!lastElementNotRemovable) {\n return;\n }\n\n const currentValue = getValues(name as Path<TFieldValues>) as unknown;\n const valueIsArray = Array.isArray(currentValue);\n const arrayValue = valueIsArray ? (currentValue as unknown[]) : [];\n const hasSingleRow = valueIsArray && arrayValue.length === 1;\n\n // Treat these as \"effectively empty after reset\":\n // - value missing/not-array\n // - empty array\n // - array where all entries are empty placeholders\n const isEffectivelyEmptyAfterReset =\n !valueIsArray ||\n arrayValue.length === 0 ||\n arrayValue.every((entry) => {\n return isValueEmpty(entry);\n });\n\n // Nothing to fix when the reset restored real values (e.g. from defaults).\n if (!isEffectivelyEmptyAfterReset) {\n return;\n }\n\n // Already normalized: exactly one row remains.\n if (hasSingleRow) {\n return;\n }\n\n // Avoid collapse animation flicker during reset normalization.\n setDisableAnimation(true);\n\n // use replace so the RHF field-array length actually collapses to one row\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n replace([elementInitialValue] as any);\n\n // Restore normal animation state right after normalization.\n if (!prefersReducedMotion) {\n setTimeout(() => {\n setDisableAnimation(false);\n }, 1);\n }\n },\n });\n\n // Initialization/Re-initialization: add one element only when array length is 0.\n // This is the min-count behavior for lastElementNotRemovable and is intentionally\n // separate from reset normalization above.\n //\n // Reset behavior in this hook is split into two phases:\n // 1) Reset normalization (useWatchFormReset): collapse stale placeholder rows\n // left by reset edge cases.\n // 2) Length-based initialization (this effect): ensure a min-one array when\n // the field array is truly empty.\n // CRITICAL: This effect MUST be the LAST hook in this component.\n // It sets hasInitialized.current = true, which acts as a gate for other effects.\n // If this runs before other effects, hasInitialized will be true during their first run,\n // causing them to execute logic meant only for post-initialization (e.g., validation,\n // animation enabling). By placing this last, all other effects run first with\n // hasInitialized = false, allowing them to skip initialization-phase logic.\n useEffect(\n () => {\n if (needsInitialize) {\n // use setValue instead of append to avoid focusing the added element\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n setValue(name as Path<TFieldValues>, [elementInitialValue] as any, {\n shouldDirty: false,\n shouldTouch: false,\n });\n\n // Mark initialization as complete\n hasInitialized.current = true;\n\n // Enable animations after a brief delay (unless user prefers reduced motion or animations are already enabled).\n // This only runs on initial mount when animations start disabled.\n // On reset, disableAnimation is typically false, so this setTimeout won't run and animations stay enabled.\n if (!prefersReducedMotion && disableAnimation) {\n setTimeout(() => {\n setDisableAnimation(false);\n }, 1);\n }\n }\n },\n // Run when needsInitialize changes (initial mount or reset)\n // needsInitialize is memoized based on fields.length and lastElementNotRemovable\n // Other dependencies are intentionally omitted:\n // - append, setValue, trigger, setDisableAnimation are stable refs/functions\n // - elementInitialValue, name, flat, prefersReducedMotion, disableAnimation are props/stable values\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [needsInitialize],\n );\n\n return {\n // Field array methods and state\n fields,\n append,\n remove,\n insert,\n move,\n disableAnimation,\n elementInitialValue,\n // Uniform field state and utilities (spread all)\n ...uniformField,\n };\n};\n","/* v8 ignore start */\n\nexport * from './useUniformFieldArray';\n\n/* v8 ignore stop */\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA8CA,MAAa,wBAEX,EACA,MACA,OAAO,OACP,qBAAqB,uBAAuB,MAC5C,0BAA0B,OAC1B,UACA,QAAQ,gBACR,YAC6C;CAE7C,MAAM,eAAe,gBAA8B;EAC3C;EACN;EACA,QAAQ;EACR;CACF,CAAC;CAED,MAAM,EAAE,YAAY;CAEpB,MAAM,EAAE,QAAQ,QAAQ,QAAQ,QAAQ,MAAM,YAAYA,cAAiB;EACzE;EACA;CACF,CAAC;CAED,MAAM,EAAE,SAAS,UAAU,cAAcC,iBAA6B;CAStE,MAAM,kBAAkB,cAAc;EACpC,OAAO,2BAA2B,OAAO,WAAW;CACtD,GAAG,CAAC,yBAAyB,OAAO,MAAM,CAAC;CAS3C,MAAM,iBAAiB,OAAO,CAAC,eAAe;CAK9C,gBAAgB;EACd,IAAI,iBACF,eAAe,UAAU;CAE7B,GAAG,CAAC,eAAe,CAAC;CAOpB,gBAAgB;EACd,IAAI,eAAe,SACjB,iBAAiB;GACf,QAAQ,IAA0B;EACpC,GAAG,GAAG;CAGV,GAAG,CAAC,OAAO,MAAM,CAAC;CAIlB,MAAM,CAAC,kBAAkB,uBAAuB,SAAS,IAAI;CAI7D,MAAM,uBAAuB,iBAAiB;CAC9C,gBAAgB;EACd,IAAI,eAAe,SACjB,oBAAoB,CAAC,CAAC,oBAAoB;CAE9C,GAAG,CAAC,oBAAoB,CAAC;CAKzB,MAAM,sBAAsB,cAAc;EACxC,OAAO,OACH,GAAG,eAAe,wBAAwB,KAAK,IAC9C,wBAAwB,CAAC;CAChC,GAAG,CAAC,MAAM,oBAAoB,CAAC;CAW/B,kBAAkB,EAChB,eAAe;EAEb,IAAI,CAAC,yBACH;EAGF,MAAM,eAAe,UAAU,IAA0B;EACzD,MAAM,eAAe,MAAM,QAAQ,YAAY;EAC/C,MAAM,aAAa,eAAgB,eAA6B,CAAC;EACjE,MAAM,eAAe,gBAAgB,WAAW,WAAW;EAc3D,IAAI,EAPF,CAAC,gBACD,WAAW,WAAW,KACtB,WAAW,OAAO,UAAU;GAC1B,OAAO,aAAa,KAAK;EAC3B,CAAC,IAID;EAIF,IAAI,cACF;EAIF,oBAAoB,IAAI;EAIxB,QAAQ,CAAC,mBAAmB,CAAQ;EAGpC,IAAI,CAAC,sBACH,iBAAiB;GACf,oBAAoB,KAAK;EAC3B,GAAG,CAAC;CAER,EACF,CAAC;CAiBD,gBACQ;EACJ,IAAI,iBAAiB;GAGnB,SAAS,MAA4B,CAAC,mBAAmB,GAAU;IACjE,aAAa;IACb,aAAa;GACf,CAAC;GAGD,eAAe,UAAU;GAKzB,IAAI,CAAC,wBAAwB,kBAC3B,iBAAiB;IACf,oBAAoB,KAAK;GAC3B,GAAG,CAAC;EAER;CACF,GAOA,CAAC,eAAe,CAClB;CAEA,OAAO;EAEL;EACA;EACA;EACA;EACA;EACA;EACA;EAEA,GAAG;CACL;AACF"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["useRHFFieldArray","useFormContext"],"sources":["../../../src/hooks/useUniformFieldArray/useUniformFieldArray.ts","../../../src/hooks/useUniformFieldArray/index.ts"],"sourcesContent":["import type { ReactNode } from 'react';\nimport type { ArrayPath, FieldValues, Path } from 'react-hook-form';\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { useFieldArray as useRHFFieldArray } from 'react-hook-form';\n\nimport { useReducedMotion } from '@fuf-stack/pixel-motion';\n\nimport { flatArrayKey, isValueEmpty } from '../../helpers';\nimport { useFormContext } from '../useFormContext';\nimport { useUniformField } from '../useUniformField';\nimport { useWatchFormReset } from '../useWatchFormReset';\n\nexport interface UseUniformFieldArrayProps<\n TFieldValues extends FieldValues = FieldValues,\n> {\n /** Field name for the array */\n name: ArrayPath<TFieldValues>;\n /** Whether this is a flat array (array of primitives) */\n flat?: boolean;\n /** Initial value for new array elements */\n elementInitialValue?: unknown;\n /** Whether the last element cannot be removed (always maintain at least one element) */\n lastElementNotRemovable?: boolean;\n /** Disable the field */\n disabled?: boolean;\n /** Optional explicit test id used to build stable test ids */\n testId?: string;\n /** Optional label content */\n label?: ReactNode;\n}\n\n/**\n * Enhanced useFieldArray hook with initialization and animation logic.\n * Based on React Hook Form's useFieldArray with additional features:\n * - Automatic initialization when lastElementNotRemovable is set\n * - Reset-only normalization for stale empty placeholder rows (via useWatchFormReset)\n * - Animation control (disabled during initialization)\n * - Temporary animation disable during reset normalization collapse\n * - Support for flat arrays (arrays of primitives)\n *\n * Note: Automatic validation triggering on length change is disabled to prevent\n * triggering form-wide validation. Array validation still runs on form submission.\n *\n * @see https://react-hook-form.com/docs/usefieldarray\n */\nexport const useUniformFieldArray = <\n TFieldValues extends FieldValues = FieldValues,\n>({\n name,\n flat = false,\n elementInitialValue: _elementInitialValue = null,\n lastElementNotRemovable = false,\n disabled,\n testId: explicitTestId,\n label,\n}: UseUniformFieldArrayProps<TFieldValues>) => {\n // Get uniform field state and utilities\n const uniformField = useUniformField<TFieldValues>({\n name: name as Path<TFieldValues> & string,\n disabled,\n testId: explicitTestId,\n label,\n });\n\n const { control } = uniformField;\n\n const { fields, append, remove, insert, move, replace } = useRHFFieldArray({\n control,\n name,\n });\n\n const { trigger, setValue, getValues } = useFormContext<TFieldValues>();\n\n // Determine if initialization is needed (initially or after reset).\n // lastElementNotRemovable is purely a minimum-count guarantee: when there are\n // no rows, add one. This handles both:\n // - Initial mount: fields.length starts at 0\n // - Form reset to an empty array: fields.length becomes 0 again\n // It intentionally does NOT inspect row contents, so manually added empty rows\n // are never collapsed.\n const needsInitialize = useMemo(() => {\n return lastElementNotRemovable && fields.length === 0;\n }, [lastElementNotRemovable, fields.length]);\n\n // Track whether initialization has completed. Initialized contextually:\n // - If initialization IS needed (needsInitialize = true): starts as false, set to true after init\n // - If initialization is NOT needed (needsInitialize = false): starts as true (already initialized)\n // This ref is used to:\n // 1. Skip validation during initialization/re-initialization\n // 2. Gate animation enabling until after initialization\n // 3. Gate motion preference effect until after initialization\n const hasInitialized = useRef(!needsInitialize);\n\n // Reset initialization flag when needsInitialize changes to true.\n // This handles form reset: when fields become empty (needsInitialize becomes true),\n // hasInitialized is reset to false, triggering re-initialization in the effect below.\n useEffect(() => {\n if (needsInitialize) {\n hasInitialized.current = false;\n }\n }, [needsInitialize]);\n\n // Validate array-level constraints (min/max items) when length changes.\n // This ensures min/max errors appear instantly when user adds/removes items.\n // Note: Child field validation also runs, but new empty fields won't show as invalid\n // because useFormContext only sets invalid=true for touched fields or after form submission.\n // Skip validation during initialization/re-initialization to avoid showing errors prematurely.\n useEffect(() => {\n if (hasInitialized.current) {\n setTimeout(() => {\n trigger(name as Path<TFieldValues>);\n }, 200);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [fields.length]);\n\n // Animation control: Start with animations disabled to prevent animating in initial elements.\n // Will be enabled after initialization completes (unless user prefers reduced motion).\n const [disableAnimation, setDisableAnimation] = useState(true);\n\n // Respond to user's motion preference changes (after initialization).\n // During initialization, animations stay disabled regardless of preference.\n const prefersReducedMotion = useReducedMotion();\n useEffect(() => {\n if (hasInitialized.current) {\n setDisableAnimation(!!prefersReducedMotion);\n }\n }, [prefersReducedMotion]);\n\n // Prepare initial element value based on mode\n // - flat=true: arrays of primitives → object with flatArrayKey and null value by default\n // - flat=false: arrays of objects → empty object by default\n const elementInitialValue = useMemo(() => {\n return flat\n ? { [flatArrayKey]: _elementInitialValue ?? null }\n : (_elementInitialValue ?? {});\n }, [flat, _elementInitialValue]);\n\n // Reset normalization:\n // Run ONLY when an actual form reset is emitted (via useWatchFormReset), not\n // on regular field edits. This does not collapse rows users add manually.\n //\n // Why this exists:\n // RHF can keep stale field-array rows after reset when array defaults are\n // missing (e.g. value becomes undefined or [null, null] while UI still has\n // multiple rows). We normalize this reset-only state to:\n // - one row when lastElementNotRemovable is enabled\n // - zero rows otherwise\n useWatchFormReset({\n onReset: () => {\n const currentValue = getValues(name as Path<TFieldValues>) as unknown;\n const valueIsArray = Array.isArray(currentValue);\n const arrayValue = valueIsArray ? (currentValue as unknown[]) : [];\n const normalizedLength = lastElementNotRemovable ? 1 : 0;\n const alreadyNormalized =\n valueIsArray && arrayValue.length === normalizedLength;\n\n // Treat these as \"effectively empty after reset\":\n // - value missing/not-array\n // - empty array\n // - array where all entries are empty placeholders\n const isEffectivelyEmptyAfterReset =\n !valueIsArray ||\n arrayValue.length === 0 ||\n arrayValue.every((entry) => {\n return isValueEmpty(entry);\n });\n\n // Nothing to fix when the reset restored real values (e.g. from defaults).\n if (!isEffectivelyEmptyAfterReset) {\n return;\n }\n\n // Already normalized to the target row count.\n if (alreadyNormalized) {\n return;\n }\n\n // Avoid collapse animation flicker during reset normalization.\n setDisableAnimation(true);\n\n // use replace so the RHF field-array length actually matches the target.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n replace((lastElementNotRemovable ? [elementInitialValue] : []) as any);\n\n // Restore normal animation state right after normalization.\n if (!prefersReducedMotion) {\n setTimeout(() => {\n setDisableAnimation(false);\n }, 1);\n }\n },\n });\n\n // Initialization/Re-initialization: add one element only when array length is 0.\n // This is the min-count behavior for lastElementNotRemovable and is intentionally\n // separate from reset normalization above.\n //\n // Reset behavior in this hook is split into two phases:\n // 1) Reset normalization (useWatchFormReset): collapse stale placeholder rows\n // left by reset edge cases.\n // 2) Length-based initialization (this effect): ensure a min-one array when\n // the field array is truly empty.\n // CRITICAL: This effect MUST be the LAST hook in this component.\n // It sets hasInitialized.current = true, which acts as a gate for other effects.\n // If this runs before other effects, hasInitialized will be true during their first run,\n // causing them to execute logic meant only for post-initialization (e.g., validation,\n // animation enabling). By placing this last, all other effects run first with\n // hasInitialized = false, allowing them to skip initialization-phase logic.\n useEffect(\n () => {\n if (needsInitialize) {\n // use setValue instead of append to avoid focusing the added element\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n setValue(name as Path<TFieldValues>, [elementInitialValue] as any, {\n shouldDirty: false,\n shouldTouch: false,\n });\n\n // Mark initialization as complete\n hasInitialized.current = true;\n\n // Enable animations after a brief delay (unless user prefers reduced motion or animations are already enabled).\n // This only runs on initial mount when animations start disabled.\n // On reset, disableAnimation is typically false, so this setTimeout won't run and animations stay enabled.\n if (!prefersReducedMotion && disableAnimation) {\n setTimeout(() => {\n setDisableAnimation(false);\n }, 1);\n }\n }\n },\n // Run when needsInitialize changes (initial mount or reset)\n // needsInitialize is memoized based on fields.length and lastElementNotRemovable\n // Other dependencies are intentionally omitted:\n // - append, setValue, trigger, setDisableAnimation are stable refs/functions\n // - elementInitialValue, name, flat, prefersReducedMotion, disableAnimation are props/stable values\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [needsInitialize],\n );\n\n return {\n // Field array methods and state\n fields,\n append,\n remove,\n insert,\n move,\n disableAnimation,\n elementInitialValue,\n // Uniform field state and utilities (spread all)\n ...uniformField,\n };\n};\n","/* v8 ignore start */\n\nexport * from './useUniformFieldArray';\n\n/* v8 ignore stop */\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA8CA,MAAa,wBAEX,EACA,MACA,OAAO,OACP,qBAAqB,uBAAuB,MAC5C,0BAA0B,OAC1B,UACA,QAAQ,gBACR,YAC6C;CAE7C,MAAM,eAAe,gBAA8B;EAC3C;EACN;EACA,QAAQ;EACR;CACF,CAAC;CAED,MAAM,EAAE,YAAY;CAEpB,MAAM,EAAE,QAAQ,QAAQ,QAAQ,QAAQ,MAAM,YAAYA,cAAiB;EACzE;EACA;CACF,CAAC;CAED,MAAM,EAAE,SAAS,UAAU,cAAcC,iBAA6B;CAStE,MAAM,kBAAkB,cAAc;EACpC,OAAO,2BAA2B,OAAO,WAAW;CACtD,GAAG,CAAC,yBAAyB,OAAO,MAAM,CAAC;CAS3C,MAAM,iBAAiB,OAAO,CAAC,eAAe;CAK9C,gBAAgB;EACd,IAAI,iBACF,eAAe,UAAU;CAE7B,GAAG,CAAC,eAAe,CAAC;CAOpB,gBAAgB;EACd,IAAI,eAAe,SACjB,iBAAiB;GACf,QAAQ,IAA0B;EACpC,GAAG,GAAG;CAGV,GAAG,CAAC,OAAO,MAAM,CAAC;CAIlB,MAAM,CAAC,kBAAkB,uBAAuB,SAAS,IAAI;CAI7D,MAAM,uBAAuB,iBAAiB;CAC9C,gBAAgB;EACd,IAAI,eAAe,SACjB,oBAAoB,CAAC,CAAC,oBAAoB;CAE9C,GAAG,CAAC,oBAAoB,CAAC;CAKzB,MAAM,sBAAsB,cAAc;EACxC,OAAO,OACH,GAAG,eAAe,wBAAwB,KAAK,IAC9C,wBAAwB,CAAC;CAChC,GAAG,CAAC,MAAM,oBAAoB,CAAC;CAY/B,kBAAkB,EAChB,eAAe;EACb,MAAM,eAAe,UAAU,IAA0B;EACzD,MAAM,eAAe,MAAM,QAAQ,YAAY;EAC/C,MAAM,aAAa,eAAgB,eAA6B,CAAC;EACjE,MAAM,mBAAmB,0BAA0B,IAAI;EACvD,MAAM,oBACJ,gBAAgB,WAAW,WAAW;EAcxC,IAAI,EAPF,CAAC,gBACD,WAAW,WAAW,KACtB,WAAW,OAAO,UAAU;GAC1B,OAAO,aAAa,KAAK;EAC3B,CAAC,IAID;EAIF,IAAI,mBACF;EAIF,oBAAoB,IAAI;EAIxB,QAAS,0BAA0B,CAAC,mBAAmB,IAAI,CAAC,CAAS;EAGrE,IAAI,CAAC,sBACH,iBAAiB;GACf,oBAAoB,KAAK;EAC3B,GAAG,CAAC;CAER,EACF,CAAC;CAiBD,gBACQ;EACJ,IAAI,iBAAiB;GAGnB,SAAS,MAA4B,CAAC,mBAAmB,GAAU;IACjE,aAAa;IACb,aAAa;GACf,CAAC;GAGD,eAAe,UAAU;GAKzB,IAAI,CAAC,wBAAwB,kBAC3B,iBAAiB;IACf,oBAAoB,KAAK;GAC3B,GAAG,CAAC;EAER;CACF,GAOA,CAAC,eAAe,CAClB;CAEA,OAAO;EAEL;EACA;EACA;EACA;EACA;EACA;EACA;EAEA,GAAG;CACL;AACF"}
|