@trackunit/react-form-components 2.6.23-alpha-3e7014314a8.0 → 2.6.26

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/index.cjs.js CHANGED
@@ -4860,7 +4860,6 @@ Search.displayName = "Search";
4860
4860
  *
4861
4861
  */
4862
4862
  const FormFieldSelectAdapter = ({ className, "data-testid": dataTestId, helpText, helpAddon, tip, label, isInvalid = false, errorMessage, name, onBlur, options, value, defaultValue, id, htmlFor: htmlForProp, onChange, children, style, ref, required = false, ...rest }) => {
4863
- const isFirstRender = reactComponents.useIsFirstRender();
4864
4863
  const [innerValue, setInnerValue] = react.useState(value ?? defaultValue);
4865
4864
  const onChangeRef = react.useRef(onChange);
4866
4865
  react.useEffect(() => {
@@ -4880,17 +4879,42 @@ const FormFieldSelectAdapter = ({ className, "data-testid": dataTestId, helpText
4880
4879
  const innerRef = react.useRef(null);
4881
4880
  // eslint-disable-next-line @trackunit/no-typescript-assertion, @typescript-eslint/no-non-null-assertion
4882
4881
  react.useImperativeHandle(ref, () => innerRef.current, []);
4882
+ // Bridges a react-select pick back to the hidden native <select> so `register`/native form
4883
+ // consumers see a real change event.
4884
+ //
4885
+ // Only picks are bridged. A value arriving from the `value` prop is something the parent already
4886
+ // holds, so reporting it back would be an echo - and through `register(...).onChange` that echo
4887
+ // is indistinguishable from typing, marking the consumer's form dirty with no user interaction.
4888
+ const lastNotifiedValueRef = react.useRef(innerValue);
4889
+ // A box, not a bare value: `null` then unambiguously means "no pick pending", so the origin gate
4890
+ // below cannot be satisfied by a parent-driven change that merely matches the ref's empty state.
4891
+ const pendingUserPickRef = react.useRef(null);
4883
4892
  react.useEffect(() => {
4884
- if (isFirstRender || innerValue === undefined) {
4893
+ if (lastNotifiedValueRef.current === innerValue) {
4885
4894
  return;
4886
4895
  }
4887
- const event = new Event("change");
4896
+ lastNotifiedValueRef.current = innerValue;
4897
+ // Keep the hidden select in sync whatever the origin - this is state, not notification.
4888
4898
  if (innerValue === "" && innerRef.current) {
4889
4899
  innerRef.current.value = ""; // Set the desired option value
4890
4900
  }
4901
+ // Keyed to the value the pick expected, not a boolean: react-select reports a re-pick of the
4902
+ // current option, which leaves `innerValue` untouched and so never runs this effect. A boolean
4903
+ // would still be set when the parent's next value arrives, and that value would be reported
4904
+ // back as a user edit.
4905
+ const pendingUserPick = pendingUserPickRef.current;
4906
+ pendingUserPickRef.current = null;
4907
+ const isUserChange = pendingUserPick !== null && pendingUserPick.value === innerValue;
4908
+ // No `undefined` guard needed: a pick is always a string or number (react-select's clear maps
4909
+ // to ""), so matching one narrows `innerValue` away from `undefined`. Only the parent can put
4910
+ // `undefined` there, and the gate above already rejects parent-driven values.
4911
+ if (!isUserChange) {
4912
+ return;
4913
+ }
4914
+ const event = new Event("change");
4891
4915
  innerRef.current?.dispatchEvent(event);
4892
4916
  onChangeRef.current?.(event);
4893
- }, [innerValue, isFirstRender]);
4917
+ }, [innerValue]);
4894
4918
  const optionsWithCurrentSelectionBackupOption = [
4895
4919
  // Add the current selection in case there's no options loaded yet (in CreatableSelect)
4896
4920
  // Also _don't_ add it if it's a duplicate
@@ -4926,7 +4950,9 @@ const FormFieldSelectAdapter = ({ className, "data-testid": dataTestId, helpText
4926
4950
  // If theres no value, set the inner value to an empty string
4927
4951
  // A native select can not have a null value
4928
4952
  // So even if react-select sends a null value, we need to convert it to an empty string
4929
- setInnerValue(!e ? "" : e.value);
4953
+ const nextValue = !e ? "" : e.value;
4954
+ pendingUserPickRef.current = { value: nextValue };
4955
+ setInnerValue(nextValue);
4930
4956
  },
4931
4957
  "data-testid": dataTestId,
4932
4958
  value: selectedOption,
package/index.esm.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import { registerTranslations, useNamespaceTranslation, NamespaceTrans } from '@trackunit/i18n-library-translation';
3
- import { IconButton, Icon, Tooltip, Button, Popover, PopoverTrigger, PopoverContent, cvaMenu, cvaMenuList, Tag, useIsTextTruncated, ZStack, MenuItem, useMeasure, useDebounce, useMergeRefs, Spinner, useScrollBlock, Text, Heading, useIsFirstRender } from '@trackunit/react-components';
3
+ import { IconButton, Icon, Tooltip, Button, Popover, PopoverTrigger, PopoverContent, cvaMenu, cvaMenuList, Tag, useIsTextTruncated, ZStack, MenuItem, useMeasure, useDebounce, useMergeRefs, Spinner, useScrollBlock, Text, Heading } from '@trackunit/react-components';
4
4
  import { useMemo, useRef, useEffect, useImperativeHandle, useState, useCallback, useLayoutEffect, isValidElement, cloneElement, useReducer, createContext, useContext, useId } from 'react';
5
5
  import { twMerge } from 'tailwind-merge';
6
6
  import { cvaMerge } from '@trackunit/css-class-variance-utilities';
@@ -4859,7 +4859,6 @@ Search.displayName = "Search";
4859
4859
  *
4860
4860
  */
4861
4861
  const FormFieldSelectAdapter = ({ className, "data-testid": dataTestId, helpText, helpAddon, tip, label, isInvalid = false, errorMessage, name, onBlur, options, value, defaultValue, id, htmlFor: htmlForProp, onChange, children, style, ref, required = false, ...rest }) => {
4862
- const isFirstRender = useIsFirstRender();
4863
4862
  const [innerValue, setInnerValue] = useState(value ?? defaultValue);
4864
4863
  const onChangeRef = useRef(onChange);
4865
4864
  useEffect(() => {
@@ -4879,17 +4878,42 @@ const FormFieldSelectAdapter = ({ className, "data-testid": dataTestId, helpText
4879
4878
  const innerRef = useRef(null);
4880
4879
  // eslint-disable-next-line @trackunit/no-typescript-assertion, @typescript-eslint/no-non-null-assertion
4881
4880
  useImperativeHandle(ref, () => innerRef.current, []);
4881
+ // Bridges a react-select pick back to the hidden native <select> so `register`/native form
4882
+ // consumers see a real change event.
4883
+ //
4884
+ // Only picks are bridged. A value arriving from the `value` prop is something the parent already
4885
+ // holds, so reporting it back would be an echo - and through `register(...).onChange` that echo
4886
+ // is indistinguishable from typing, marking the consumer's form dirty with no user interaction.
4887
+ const lastNotifiedValueRef = useRef(innerValue);
4888
+ // A box, not a bare value: `null` then unambiguously means "no pick pending", so the origin gate
4889
+ // below cannot be satisfied by a parent-driven change that merely matches the ref's empty state.
4890
+ const pendingUserPickRef = useRef(null);
4882
4891
  useEffect(() => {
4883
- if (isFirstRender || innerValue === undefined) {
4892
+ if (lastNotifiedValueRef.current === innerValue) {
4884
4893
  return;
4885
4894
  }
4886
- const event = new Event("change");
4895
+ lastNotifiedValueRef.current = innerValue;
4896
+ // Keep the hidden select in sync whatever the origin - this is state, not notification.
4887
4897
  if (innerValue === "" && innerRef.current) {
4888
4898
  innerRef.current.value = ""; // Set the desired option value
4889
4899
  }
4900
+ // Keyed to the value the pick expected, not a boolean: react-select reports a re-pick of the
4901
+ // current option, which leaves `innerValue` untouched and so never runs this effect. A boolean
4902
+ // would still be set when the parent's next value arrives, and that value would be reported
4903
+ // back as a user edit.
4904
+ const pendingUserPick = pendingUserPickRef.current;
4905
+ pendingUserPickRef.current = null;
4906
+ const isUserChange = pendingUserPick !== null && pendingUserPick.value === innerValue;
4907
+ // No `undefined` guard needed: a pick is always a string or number (react-select's clear maps
4908
+ // to ""), so matching one narrows `innerValue` away from `undefined`. Only the parent can put
4909
+ // `undefined` there, and the gate above already rejects parent-driven values.
4910
+ if (!isUserChange) {
4911
+ return;
4912
+ }
4913
+ const event = new Event("change");
4890
4914
  innerRef.current?.dispatchEvent(event);
4891
4915
  onChangeRef.current?.(event);
4892
- }, [innerValue, isFirstRender]);
4916
+ }, [innerValue]);
4893
4917
  const optionsWithCurrentSelectionBackupOption = [
4894
4918
  // Add the current selection in case there's no options loaded yet (in CreatableSelect)
4895
4919
  // Also _don't_ add it if it's a duplicate
@@ -4925,7 +4949,9 @@ const FormFieldSelectAdapter = ({ className, "data-testid": dataTestId, helpText
4925
4949
  // If theres no value, set the inner value to an empty string
4926
4950
  // A native select can not have a null value
4927
4951
  // So even if react-select sends a null value, we need to convert it to an empty string
4928
- setInnerValue(!e ? "" : e.value);
4952
+ const nextValue = !e ? "" : e.value;
4953
+ pendingUserPickRef.current = { value: nextValue };
4954
+ setInnerValue(nextValue);
4929
4955
  },
4930
4956
  "data-testid": dataTestId,
4931
4957
  value: selectedOption,
@@ -1,11 +1,41 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.insertAttributeIntoOpeningTag = exports.logSummary = exports.tsquery = exports.parseTsx = exports.hasSpreadAttribute = exports.findJsxAttribute = exports.findJsxElements = exports.getLocalAliasFor = exports.getImportedAliases = exports.visitTsFiles = exports.visitTsxFiles = void 0;
4
- const tslib_1 = require("tslib");
36
+ exports.insertAttributeIntoOpeningTag = exports.logSummary = exports.parseTsx = exports.hasSpreadAttribute = exports.findJsxAttribute = exports.findJsxElements = exports.getLocalAliasFor = exports.getImportedAliases = exports.visitTsFiles = exports.visitTsxFiles = void 0;
5
37
  const devkit_1 = require("@nx/devkit");
6
- const tsquery_1 = require("@phenomnomnominal/tsquery");
7
- Object.defineProperty(exports, "tsquery", { enumerable: true, get: function () { return tsquery_1.tsquery; } });
8
- const ts = tslib_1.__importStar(require("typescript"));
38
+ const ts = __importStar(require("typescript"));
9
39
  const TSX_EXTENSIONS = [".tsx", ".jsx"];
10
40
  const TS_EXTENSIONS = [".ts", ".tsx", ".jsx"];
11
41
  const isTsxFile = (filePath) => TSX_EXTENSIONS.some(ext => filePath.endsWith(ext));
@@ -1,9 +1,41 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.actionButtonAddTitle = void 0;
4
- const tslib_1 = require("tslib");
5
37
  const devkit_1 = require("@nx/devkit");
6
- const ts = tslib_1.__importStar(require("typescript"));
38
+ const ts = __importStar(require("typescript"));
7
39
  const jsx_utils_1 = require("../utils/jsx-utils");
8
40
  const PACKAGE_NAME = "@trackunit/react-form-components";
9
41
  const COMPONENT_NAME = "ActionButton";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-form-components",
3
- "version": "2.6.23-alpha-3e7014314a8.0",
3
+ "version": "2.6.26",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "migrations": "./migrations.json",
@@ -10,17 +10,17 @@
10
10
  "dependencies": {
11
11
  "react-calendar": "^6.0.0",
12
12
  "react-select": "^5.10.2",
13
- "@trackunit/date-and-time-utils": "1.14.21-alpha-3e7014314a8.0",
13
+ "@trackunit/date-and-time-utils": "1.14.20",
14
14
  "usehooks-ts": "^3.1.0",
15
15
  "libphonenumber-js": "^1.13.6",
16
16
  "zod": "^3.25.76",
17
17
  "tailwind-merge": "^2.0.0",
18
- "@trackunit/css-class-variance-utilities": "1.14.20-alpha-3e7014314a8.0",
19
- "@trackunit/react-components": "2.10.16-alpha-3e7014314a8.0",
20
- "@trackunit/ui-icons": "1.14.19-alpha-3e7014314a8.0",
21
- "@trackunit/shared-utils": "1.16.23-alpha-3e7014314a8.0",
22
- "@trackunit/ui-design-tokens": "1.15.10-alpha-3e7014314a8.0",
23
- "@trackunit/i18n-library-translation": "2.4.22-alpha-3e7014314a8.0",
18
+ "@trackunit/css-class-variance-utilities": "1.14.19",
19
+ "@trackunit/react-components": "2.10.19",
20
+ "@trackunit/ui-icons": "1.14.18",
21
+ "@trackunit/shared-utils": "1.16.22",
22
+ "@trackunit/ui-design-tokens": "1.15.9",
23
+ "@trackunit/i18n-library-translation": "2.4.25",
24
24
  "string-ts": "^2.0.0",
25
25
  "es-toolkit": "^1.39.10"
26
26
  },
@@ -32,6 +32,11 @@ export interface SelectFieldProps<TOption extends BaseOptionType = BaseOptionTyp
32
32
  /**
33
33
  * The onChange handler for the SelectField.
34
34
  *
35
+ * Called when the user picks a different option. It is not called when the `value` prop changes,
36
+ * since that value is one the parent already holds, and not when the user re-picks the option that
37
+ * is already selected. It is also forwarded to the hidden native `<select>`, so a change event
38
+ * dispatched on that node invokes it too.
39
+ *
35
40
  * @important
36
41
  */
37
42
  onChange?: (event: {