@cdx-ui/utils 0.0.1-beta.64 → 0.0.1-beta.66
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/lib/commonjs/common/mergeRefs.js +39 -1
- package/lib/commonjs/common/mergeRefs.js.map +1 -1
- package/lib/commonjs/form-control/useFormControl.js +10 -0
- package/lib/commonjs/form-control/useFormControl.js.map +1 -1
- package/lib/module/common/mergeRefs.js +39 -1
- package/lib/module/common/mergeRefs.js.map +1 -1
- package/lib/module/form-control/index.js.map +1 -1
- package/lib/module/form-control/useFormControl.js +11 -0
- package/lib/module/form-control/useFormControl.js.map +1 -1
- package/lib/typescript/common/mergeRefs.d.ts +8 -0
- package/lib/typescript/common/mergeRefs.d.ts.map +1 -1
- package/lib/typescript/form-control/index.d.ts +1 -1
- package/lib/typescript/form-control/index.d.ts.map +1 -1
- package/lib/typescript/form-control/useFormControl.d.ts +18 -3
- package/lib/typescript/form-control/useFormControl.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/mergeRefs.test.ts +108 -0
- package/src/common/mergeRefs.ts +45 -2
- package/src/form-control/index.ts +1 -0
- package/src/form-control/useFormControl.tsx +21 -4
|
@@ -4,15 +4,53 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.mergeRefs = mergeRefs;
|
|
7
|
+
/**
|
|
8
|
+
* Merges multiple refs (object or callback) into a single callback ref.
|
|
9
|
+
*
|
|
10
|
+
* Supports React 19 ref-cleanup semantics: if an inner callback ref returns a
|
|
11
|
+
* cleanup function, that cleanup is captured and invoked when the merged ref is
|
|
12
|
+
* detached — either via the returned cleanup (React 19) or via a subsequent
|
|
13
|
+
* call with `null` (React 18 backward-compat path).
|
|
14
|
+
*/
|
|
7
15
|
function mergeRefs(...refs) {
|
|
8
16
|
return value => {
|
|
17
|
+
if (value === null) {
|
|
18
|
+
for (const ref of refs) {
|
|
19
|
+
if (typeof ref === 'function') {
|
|
20
|
+
ref(null);
|
|
21
|
+
} else if (ref != null) {
|
|
22
|
+
ref.current = null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const cleanups = [];
|
|
9
28
|
for (const ref of refs) {
|
|
10
29
|
if (typeof ref === 'function') {
|
|
11
|
-
ref(value);
|
|
30
|
+
cleanups.push(ref(value));
|
|
12
31
|
} else if (ref != null) {
|
|
13
32
|
ref.current = value;
|
|
33
|
+
cleanups.push(undefined);
|
|
34
|
+
} else {
|
|
35
|
+
cleanups.push(undefined);
|
|
14
36
|
}
|
|
15
37
|
}
|
|
38
|
+
return () => {
|
|
39
|
+
for (let i = 0; i < refs.length; i++) {
|
|
40
|
+
const ref = refs[i];
|
|
41
|
+
if (ref == null) continue;
|
|
42
|
+
if (typeof ref === 'function') {
|
|
43
|
+
const refCleanup = cleanups[i];
|
|
44
|
+
if (typeof refCleanup === 'function') {
|
|
45
|
+
refCleanup();
|
|
46
|
+
} else {
|
|
47
|
+
ref(null);
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
ref.current = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
16
54
|
};
|
|
17
55
|
}
|
|
18
56
|
//# sourceMappingURL=mergeRefs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["mergeRefs","refs","value","ref","current"],"sourceRoot":"../../../src","sources":["common/mergeRefs.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"names":["mergeRefs","refs","value","ref","current","undefined","cleanups","push","i","length","refCleanup"],"sourceRoot":"../../../src","sources":["common/mergeRefs.ts"],"mappings":";;;;;;AAIA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASA,SAASA,CAAI,GAAGC,IAA4B,EAAkB;EAC5E,OAAQC,KAAe,IAA4B;IACjD,IAAIA,KAAK,KAAK,IAAI,EAAE;MAClB,KAAK,MAAMC,GAAG,IAAIF,IAAI,EAAE;QACtB,IAAI,OAAOE,GAAG,KAAK,UAAU,EAAE;UAC7BA,GAAG,CAAC,IAAI,CAAC;QACX,CAAC,MAAM,IAAIA,GAAG,IAAI,IAAI,EAAE;UACtBA,GAAG,CAACC,OAAO,GAAG,IAAI;QACpB;MACF;MACA,OAAOC,SAAS;IAClB;IAEA,MAAMC,QAAmC,GAAG,EAAE;IAC9C,KAAK,MAAMH,GAAG,IAAIF,IAAI,EAAE;MACtB,IAAI,OAAOE,GAAG,KAAK,UAAU,EAAE;QAC7BG,QAAQ,CAACC,IAAI,CAACJ,GAAG,CAACD,KAAK,CAA0B,CAAC;MACpD,CAAC,MAAM,IAAIC,GAAG,IAAI,IAAI,EAAE;QACtBA,GAAG,CAACC,OAAO,GAAGF,KAAK;QACnBI,QAAQ,CAACC,IAAI,CAACF,SAAS,CAAC;MAC1B,CAAC,MAAM;QACLC,QAAQ,CAACC,IAAI,CAACF,SAAS,CAAC;MAC1B;IACF;IAEA,OAAO,MAAM;MACX,KAAK,IAAIG,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGP,IAAI,CAACQ,MAAM,EAAED,CAAC,EAAE,EAAE;QACpC,MAAML,GAAG,GAAGF,IAAI,CAACO,CAAC,CAAC;QACnB,IAAIL,GAAG,IAAI,IAAI,EAAE;QAEjB,IAAI,OAAOA,GAAG,KAAK,UAAU,EAAE;UAC7B,MAAMO,UAAU,GAAGJ,QAAQ,CAACE,CAAC,CAAC;UAC9B,IAAI,OAAOE,UAAU,KAAK,UAAU,EAAE;YACpCA,UAAU,CAAC,CAAC;UACd,CAAC,MAAM;YACLP,GAAG,CAAC,IAAI,CAAC;UACX;QACF,CAAC,MAAM;UACLA,GAAG,CAACC,OAAO,GAAG,IAAI;QACpB;MACF;IACF,CAAC;EACH,CAAC;AACH","ignoreList":[]}
|
|
@@ -12,6 +12,16 @@ var _react = _interopRequireDefault(require("react"));
|
|
|
12
12
|
var _reactNative = require("react-native");
|
|
13
13
|
var _accessibilityUtils = require("../common/accessibilityUtils");
|
|
14
14
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
15
|
+
/**
|
|
16
|
+
* Minimal contract for any control registered on `FormControlContextValue.inputRef`.
|
|
17
|
+
*
|
|
18
|
+
* `Field.Label` / `focusInput()` only need to call `.focus()` on the registered node;
|
|
19
|
+
* narrower control-specific shapes (`TextInput`, `HTMLInputElement`, custom focus
|
|
20
|
+
* bridges like `Select.Trigger`'s) all satisfy this contract. Consumers that need
|
|
21
|
+
* more (e.g. reading `.value`) should cast locally — keeping the context type
|
|
22
|
+
* minimal avoids a typed lie when a non-input bridge is registered.
|
|
23
|
+
*/
|
|
24
|
+
|
|
15
25
|
const FormControlContext = exports.FormControlContext = /*#__PURE__*/_react.default.createContext({});
|
|
16
26
|
|
|
17
27
|
/** Props for the FormControl root; known fields plus passthrough to the styled root box. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_react","_interopRequireDefault","require","_reactNative","_accessibilityUtils","e","__esModule","default","FormControlContext","exports","React","createContext","useFormControlRoot","props","id","idProp","name","isRequired","isInvalid","isDisabled","isReadOnly","htmlProps","reactId","useId","labelId","feedbackId","helpTextId","hasFeedbackText","setHasFeedbackText","useState","hasHelpText","setHasHelpText","isLabelFocused","setIsLabelFocused","inputRef","useRef","focusInput","useCallback","node","current","focus","context","Boolean","Platform","OS","useReportFormControlLabelFocus","isActive","useFormControlContext","useLayoutEffect","undefined","useFormControl","field","describedBy","push","ariaDescribedBy","join","cleanProps","disabled","readOnly","required","ariaAttr","useContext"],"sourceRoot":"../../../src","sources":["form-control/useFormControl.tsx"],"mappings":";;;;;;;;;;AAAA,IAAAA,MAAA,GAAAC,sBAAA,CAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;AACA,IAAAE,mBAAA,GAAAF,OAAA;AAAwD,SAAAD,uBAAAI,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAE,OAAA,EAAAF,CAAA;
|
|
1
|
+
{"version":3,"names":["_react","_interopRequireDefault","require","_reactNative","_accessibilityUtils","e","__esModule","default","FormControlContext","exports","React","createContext","useFormControlRoot","props","id","idProp","name","isRequired","isInvalid","isDisabled","isReadOnly","htmlProps","reactId","useId","labelId","feedbackId","helpTextId","hasFeedbackText","setHasFeedbackText","useState","hasHelpText","setHasHelpText","isLabelFocused","setIsLabelFocused","inputRef","useRef","focusInput","useCallback","node","current","focus","context","Boolean","Platform","OS","useReportFormControlLabelFocus","isActive","useFormControlContext","useLayoutEffect","undefined","useFormControl","field","describedBy","push","ariaDescribedBy","join","cleanProps","disabled","readOnly","required","ariaAttr","useContext"],"sourceRoot":"../../../src","sources":["form-control/useFormControl.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,14 +1,52 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Merges multiple refs (object or callback) into a single callback ref.
|
|
5
|
+
*
|
|
6
|
+
* Supports React 19 ref-cleanup semantics: if an inner callback ref returns a
|
|
7
|
+
* cleanup function, that cleanup is captured and invoked when the merged ref is
|
|
8
|
+
* detached — either via the returned cleanup (React 19) or via a subsequent
|
|
9
|
+
* call with `null` (React 18 backward-compat path).
|
|
10
|
+
*/
|
|
3
11
|
export function mergeRefs(...refs) {
|
|
4
12
|
return value => {
|
|
13
|
+
if (value === null) {
|
|
14
|
+
for (const ref of refs) {
|
|
15
|
+
if (typeof ref === 'function') {
|
|
16
|
+
ref(null);
|
|
17
|
+
} else if (ref != null) {
|
|
18
|
+
ref.current = null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const cleanups = [];
|
|
5
24
|
for (const ref of refs) {
|
|
6
25
|
if (typeof ref === 'function') {
|
|
7
|
-
ref(value);
|
|
26
|
+
cleanups.push(ref(value));
|
|
8
27
|
} else if (ref != null) {
|
|
9
28
|
ref.current = value;
|
|
29
|
+
cleanups.push(undefined);
|
|
30
|
+
} else {
|
|
31
|
+
cleanups.push(undefined);
|
|
10
32
|
}
|
|
11
33
|
}
|
|
34
|
+
return () => {
|
|
35
|
+
for (let i = 0; i < refs.length; i++) {
|
|
36
|
+
const ref = refs[i];
|
|
37
|
+
if (ref == null) continue;
|
|
38
|
+
if (typeof ref === 'function') {
|
|
39
|
+
const refCleanup = cleanups[i];
|
|
40
|
+
if (typeof refCleanup === 'function') {
|
|
41
|
+
refCleanup();
|
|
42
|
+
} else {
|
|
43
|
+
ref(null);
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
ref.current = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
12
50
|
};
|
|
13
51
|
}
|
|
14
52
|
//# sourceMappingURL=mergeRefs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["mergeRefs","refs","value","ref","current"],"sourceRoot":"../../../src","sources":["common/mergeRefs.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"names":["mergeRefs","refs","value","ref","current","undefined","cleanups","push","i","length","refCleanup"],"sourceRoot":"../../../src","sources":["common/mergeRefs.ts"],"mappings":";;AAIA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASA,SAASA,CAAI,GAAGC,IAA4B,EAAkB;EAC5E,OAAQC,KAAe,IAA4B;IACjD,IAAIA,KAAK,KAAK,IAAI,EAAE;MAClB,KAAK,MAAMC,GAAG,IAAIF,IAAI,EAAE;QACtB,IAAI,OAAOE,GAAG,KAAK,UAAU,EAAE;UAC7BA,GAAG,CAAC,IAAI,CAAC;QACX,CAAC,MAAM,IAAIA,GAAG,IAAI,IAAI,EAAE;UACtBA,GAAG,CAACC,OAAO,GAAG,IAAI;QACpB;MACF;MACA,OAAOC,SAAS;IAClB;IAEA,MAAMC,QAAmC,GAAG,EAAE;IAC9C,KAAK,MAAMH,GAAG,IAAIF,IAAI,EAAE;MACtB,IAAI,OAAOE,GAAG,KAAK,UAAU,EAAE;QAC7BG,QAAQ,CAACC,IAAI,CAACJ,GAAG,CAACD,KAAK,CAA0B,CAAC;MACpD,CAAC,MAAM,IAAIC,GAAG,IAAI,IAAI,EAAE;QACtBA,GAAG,CAACC,OAAO,GAAGF,KAAK;QACnBI,QAAQ,CAACC,IAAI,CAACF,SAAS,CAAC;MAC1B,CAAC,MAAM;QACLC,QAAQ,CAACC,IAAI,CAACF,SAAS,CAAC;MAC1B;IACF;IAEA,OAAO,MAAM;MACX,KAAK,IAAIG,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGP,IAAI,CAACQ,MAAM,EAAED,CAAC,EAAE,EAAE;QACpC,MAAML,GAAG,GAAGF,IAAI,CAACO,CAAC,CAAC;QACnB,IAAIL,GAAG,IAAI,IAAI,EAAE;QAEjB,IAAI,OAAOA,GAAG,KAAK,UAAU,EAAE;UAC7B,MAAMO,UAAU,GAAGJ,QAAQ,CAACE,CAAC,CAAC;UAC9B,IAAI,OAAOE,UAAU,KAAK,UAAU,EAAE;YACpCA,UAAU,CAAC,CAAC;UACd,CAAC,MAAM;YACLP,GAAG,CAAC,IAAI,CAAC;UACX;QACF,CAAC,MAAM;UACLA,GAAG,CAACC,OAAO,GAAG,IAAI;QACpB;MACF;IACF,CAAC;EACH,CAAC;AACH","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["FormControlContext","useFormControl","useFormControlContext","useFormControlRoot","useReportFormControlLabelFocus"],"sourceRoot":"../../../src","sources":["form-control/index.ts"],"mappings":";;AAAA,
|
|
1
|
+
{"version":3,"names":["FormControlContext","useFormControl","useFormControlContext","useFormControlRoot","useReportFormControlLabelFocus"],"sourceRoot":"../../../src","sources":["form-control/index.ts"],"mappings":";;AAAA,SAOEA,kBAAkB,EAClBC,cAAc,EACdC,qBAAqB,EACrBC,kBAAkB,EAClBC,8BAA8B,QACzB,kBAAkB","ignoreList":[]}
|
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { Platform } from 'react-native';
|
|
5
5
|
import { ariaAttr } from '../common/accessibilityUtils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Minimal contract for any control registered on `FormControlContextValue.inputRef`.
|
|
9
|
+
*
|
|
10
|
+
* `Field.Label` / `focusInput()` only need to call `.focus()` on the registered node;
|
|
11
|
+
* narrower control-specific shapes (`TextInput`, `HTMLInputElement`, custom focus
|
|
12
|
+
* bridges like `Select.Trigger`'s) all satisfy this contract. Consumers that need
|
|
13
|
+
* more (e.g. reading `.value`) should cast locally — keeping the context type
|
|
14
|
+
* minimal avoids a typed lie when a non-input bridge is registered.
|
|
15
|
+
*/
|
|
16
|
+
|
|
6
17
|
export const FormControlContext = /*#__PURE__*/React.createContext({});
|
|
7
18
|
|
|
8
19
|
/** Props for the FormControl root; known fields plus passthrough to the styled root box. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["React","Platform","ariaAttr","FormControlContext","createContext","useFormControlRoot","props","id","idProp","name","isRequired","isInvalid","isDisabled","isReadOnly","htmlProps","reactId","useId","labelId","feedbackId","helpTextId","hasFeedbackText","setHasFeedbackText","useState","hasHelpText","setHasHelpText","isLabelFocused","setIsLabelFocused","inputRef","useRef","focusInput","useCallback","node","current","focus","context","Boolean","OS","useReportFormControlLabelFocus","isActive","useFormControlContext","useLayoutEffect","undefined","useFormControl","field","describedBy","push","ariaDescribedBy","join","cleanProps","disabled","readOnly","required","useContext"],"sourceRoot":"../../../src","sources":["form-control/useFormControl.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,SAASC,QAAQ,
|
|
1
|
+
{"version":3,"names":["React","Platform","ariaAttr","FormControlContext","createContext","useFormControlRoot","props","id","idProp","name","isRequired","isInvalid","isDisabled","isReadOnly","htmlProps","reactId","useId","labelId","feedbackId","helpTextId","hasFeedbackText","setHasFeedbackText","useState","hasHelpText","setHasHelpText","isLabelFocused","setIsLabelFocused","inputRef","useRef","focusInput","useCallback","node","current","focus","context","Boolean","OS","useReportFormControlLabelFocus","isActive","useFormControlContext","useLayoutEffect","undefined","useFormControl","field","describedBy","push","ariaDescribedBy","join","cleanProps","disabled","readOnly","required","useContext"],"sourceRoot":"../../../src","sources":["form-control/useFormControl.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,SAASC,QAAQ,QAAQ,cAAc;AACvC,SAASC,QAAQ,QAAQ,8BAA8B;;AAIvD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAsCA,OAAO,MAAMC,kBAAkB,gBAAGH,KAAK,CAACI,aAAa,CAAmC,CAAC,CAAC,CAAC;;AAM3F;;AA2BA;AACA;AACA;AACA;AACA,OAAO,SAASC,kBAAkBA,CAACC,KAA2B,EAAyB;EACrF,MAAM;IAAEC,EAAE,EAAEC,MAAM;IAAEC,IAAI;IAAEC,UAAU;IAAEC,SAAS;IAAEC,UAAU;IAAEC,UAAU;IAAE,GAAGC;EAAU,CAAC,GAAGR,KAAK;EAE/F,MAAMS,OAAO,GAAGf,KAAK,CAACgB,KAAK,CAAC,CAAC;EAC7B,MAAMT,EAAE,GAAG,CAAC,OAAOC,MAAM,KAAK,QAAQ,IAAIA,MAAM,KAAK,EAAE,GAAGA,MAAM,GAAG,IAAI,KAAK,SAASO,OAAO,EAAE;EAE9F,MAAME,OAAO,GAAG,GAAGV,EAAE,QAAQ;EAC7B,MAAMW,UAAU,GAAG,GAAGX,EAAE,WAAW;EACnC,MAAMY,UAAU,GAAG,GAAGZ,EAAE,WAAW;;EAEnC;AACF;AACA;AACA;EACE,MAAM,CAACa,eAAe,EAAEC,kBAAkB,CAAC,GAAGrB,KAAK,CAACsB,QAAQ,CAAC,KAAK,CAAC;;EAEnE;AACF;AACA;AACA;EACE,MAAM,CAACC,WAAW,EAAEC,cAAc,CAAC,GAAGxB,KAAK,CAACsB,QAAQ,CAAC,KAAK,CAAC;EAE3D,MAAM,CAACG,cAAc,EAAEC,iBAAiB,CAAC,GAAG1B,KAAK,CAACsB,QAAQ,CAAC,KAAK,CAAC;EAEjE,MAAMK,QAAQ,GAAG3B,KAAK,CAAC4B,MAAM,CAAmB,IAAI,CAAC;EAErD,MAAMC,UAAU,GAAG7B,KAAK,CAAC8B,WAAW,CAAC,MAAM;IACzC,MAAMC,IAAI,GAAGJ,QAAQ,CAACK,OAAO;IAC7B,IAAID,IAAI,IAAI,IAAI,IAAI,OAAOA,IAAI,CAACE,KAAK,KAAK,UAAU,EAAE;MACpDF,IAAI,CAACE,KAAK,CAAC,CAAC;IACd;EACF,CAAC,EAAE,EAAE,CAAC;EAEN,MAAMC,OAA8B,GAAG;IACrCxB,UAAU,EAAEyB,OAAO,CAACzB,UAAU,CAAC;IAC/BC,SAAS,EAAEwB,OAAO,CAACxB,SAAS,CAAC;IAC7BE,UAAU,EAAEsB,OAAO,CAACtB,UAAU,CAAC;IAC/BD,UAAU,EAAEuB,OAAO,CAACvB,UAAU,CAAC;IAC/BQ,eAAe;IACfC,kBAAkB;IAClBE,WAAW;IACXC,cAAc;IACdf,IAAI;IACJF,EAAE;IACFU,OAAO;IACPC,UAAU;IACVC,UAAU;IACVL,SAAS;IACTW,cAAc;IACdC,iBAAiB;IACjB,IAAIzB,QAAQ,CAACmC,EAAE,KAAK,KAAK,GAAG;MAAET,QAAQ;MAAEE;IAAW,CAAC,GAAG,CAAC,CAAC;EAC3D,CAAC;EAED,OAAOK,OAAO;AAChB;;AAEA;AACA;AACA;AACA;AACA,OAAO,SAASG,8BAA8BA,CAACC,QAAiB,EAAE;EAChE,MAAM;IAAEZ;EAAkB,CAAC,GAAGa,qBAAqB,CAAC,CAAC;EAErDvC,KAAK,CAACwC,eAAe,CAAC,MAAM;IAC1B,IAAI,OAAOd,iBAAiB,KAAK,UAAU,EAAE;MAC3C,OAAOe,SAAS;IAClB;IACAf,iBAAiB,CAACY,QAAQ,CAAC;IAC3B,OAAO,MAAM;MACXZ,iBAAiB,CAAC,KAAK,CAAC;IAC1B,CAAC;EACH,CAAC,EAAE,CAACY,QAAQ,EAAEZ,iBAAiB,CAAC,CAAC;AACnC;;AAEA;AACA;AACA;AACA;AACA,OAAO,SAASgB,cAAcA,CAACpC,KAA4B,EAAE;EAC3D,MAAMqC,KAAK,GAAGJ,qBAAqB,CAAC,CAAC;EACrC,MAAMK,WAAqB,GAAG,EAAE;EAEhC,IAAID,KAAK,CAACvB,eAAe,IAAIuB,KAAK,CAACzB,UAAU,EAAE;IAC7C0B,WAAW,CAACC,IAAI,CAACF,KAAK,CAACzB,UAAU,CAAC;EACpC;EACA,IAAIyB,KAAK,CAACpB,WAAW,IAAIoB,KAAK,CAACxB,UAAU,EAAE;IACzCyB,WAAW,CAACC,IAAI,CAACF,KAAK,CAACxB,UAAU,CAAC;EACpC;EACA,MAAM2B,eAAe,GAAGF,WAAW,CAACG,IAAI,CAAC,GAAG,CAAC;EAE7C,MAAM;IAAEpC,SAAS;IAAEC,UAAU;IAAEC,UAAU;IAAEH,UAAU;IAAE,GAAGsC;EAAW,CAAC,GAAG1C,KAAK;EAC9E,MAAMC,EAAE,GAAGD,KAAK,CAACC,EAAE,KAAKoC,KAAK,CAACpC,EAAE,GAAG,GAAGoC,KAAK,CAACpC,EAAE,QAAQ,GAAGkC,SAAS,CAAC;EAEnE,OAAO;IACL,GAAGO,UAAU;IACbzC,EAAE;IACFE,IAAI,EAAEkC,KAAK,CAAClC,IAAI;IAChBwC,QAAQ,EAAErC,UAAU,IAAI+B,KAAK,CAAC/B,UAAU;IACxCsC,QAAQ,EAAErC,UAAU,IAAI8B,KAAK,CAAC9B,UAAU;IACxCsC,QAAQ,EAAEzC,UAAU,IAAIiC,KAAK,CAACjC,UAAU;IACxC,cAAc,EAAER,QAAQ,CAACS,SAAS,IAAIgC,KAAK,CAAChC,SAAS,CAAC;IACtD,eAAe,EAAET,QAAQ,CAACQ,UAAU,IAAIiC,KAAK,CAACjC,UAAU,CAAC;IACzD,eAAe,EAAER,QAAQ,CAACW,UAAU,IAAI8B,KAAK,CAAC9B,UAAU,CAAC;IACzD,kBAAkB,EAAEiC,eAAe,IAAIL,SAAS;IAChD,iBAAiB,EAAEE,KAAK,CAAC1B;EAC3B,CAAC;AACH;AAEA,OAAO,MAAMsB,qBAAqB,GAAGA,CAAA,KAAM;EACzC,OAAOvC,KAAK,CAACoD,UAAU,CAACjD,kBAAkB,CAAC;AAC7C,CAAC","ignoreList":[]}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import type { Ref, RefCallback } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Merges multiple refs (object or callback) into a single callback ref.
|
|
4
|
+
*
|
|
5
|
+
* Supports React 19 ref-cleanup semantics: if an inner callback ref returns a
|
|
6
|
+
* cleanup function, that cleanup is captured and invoked when the merged ref is
|
|
7
|
+
* detached — either via the returned cleanup (React 19) or via a subsequent
|
|
8
|
+
* call with `null` (React 18 backward-compat path).
|
|
9
|
+
*/
|
|
2
10
|
export declare function mergeRefs<T>(...refs: (Ref<T> | undefined)[]): RefCallback<T>;
|
|
3
11
|
//# sourceMappingURL=mergeRefs.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mergeRefs.d.ts","sourceRoot":"","sources":["../../../src/common/mergeRefs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"mergeRefs.d.ts","sourceRoot":"","sources":["../../../src/common/mergeRefs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAI9C;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,EAAE,GAAG,WAAW,CAAC,CAAC,CAAC,CA2C5E"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { type FormControlContextValue, type FormControlFieldProps, type FormControlRootProps, type FormControlRootReturn, type FormControlState, FormControlContext, useFormControl, useFormControlContext, useFormControlRoot, useReportFormControlLabelFocus, } from './useFormControl';
|
|
1
|
+
export { type Focusable, type FormControlContextValue, type FormControlFieldProps, type FormControlRootProps, type FormControlRootReturn, type FormControlState, FormControlContext, useFormControl, useFormControlContext, useFormControlRoot, useReportFormControlLabelFocus, } from './useFormControl';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/form-control/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,kBAAkB,EAClB,cAAc,EACd,qBAAqB,EACrB,kBAAkB,EAClB,8BAA8B,GAC/B,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/form-control/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EACd,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,kBAAkB,EAClB,cAAc,EACd,qBAAqB,EACrB,kBAAkB,EAClB,8BAA8B,GAC/B,MAAM,kBAAkB,CAAC"}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { type TextInput } from 'react-native';
|
|
3
2
|
type HTMLProps = Record<string, unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* Minimal contract for any control registered on `FormControlContextValue.inputRef`.
|
|
5
|
+
*
|
|
6
|
+
* `Field.Label` / `focusInput()` only need to call `.focus()` on the registered node;
|
|
7
|
+
* narrower control-specific shapes (`TextInput`, `HTMLInputElement`, custom focus
|
|
8
|
+
* bridges like `Select.Trigger`'s) all satisfy this contract. Consumers that need
|
|
9
|
+
* more (e.g. reading `.value`) should cast locally — keeping the context type
|
|
10
|
+
* minimal avoids a typed lie when a non-input bridge is registered.
|
|
11
|
+
*/
|
|
12
|
+
export interface Focusable {
|
|
13
|
+
focus(): void;
|
|
14
|
+
}
|
|
4
15
|
export interface FormControlState {
|
|
5
16
|
isRequired: boolean;
|
|
6
17
|
isInvalid: boolean;
|
|
@@ -21,8 +32,12 @@ export interface FormControlContextValue extends FormControlState {
|
|
|
21
32
|
/**
|
|
22
33
|
* @platform native — set by `Input.Field` / `Select.Trigger` for label press-to-focus.
|
|
23
34
|
* On web, use `<label htmlFor>` with a labelable control (`input`, `button`, `select`, …).
|
|
35
|
+
*
|
|
36
|
+
* Typed as `Focusable` so any control (TextInput, HTMLInputElement, custom focus
|
|
37
|
+
* bridge) can register without a cast at the assignment site. Consumers that need
|
|
38
|
+
* the narrower shape cast locally.
|
|
24
39
|
*/
|
|
25
|
-
inputRef?: React.RefObject<
|
|
40
|
+
inputRef?: React.RefObject<Focusable | null>;
|
|
26
41
|
focusInput?: () => void;
|
|
27
42
|
/** True when the primary child control is focused or (for Select) open — drives `Field.Label` styling. */
|
|
28
43
|
isLabelFocused?: boolean;
|
|
@@ -52,7 +67,7 @@ export interface FormControlRootReturn {
|
|
|
52
67
|
feedbackId: string;
|
|
53
68
|
helpTextId: string;
|
|
54
69
|
htmlProps: HTMLProps;
|
|
55
|
-
inputRef?: React.RefObject<
|
|
70
|
+
inputRef?: React.RefObject<Focusable | null>;
|
|
56
71
|
focusInput?: () => void;
|
|
57
72
|
isLabelFocused: boolean;
|
|
58
73
|
setIsLabelFocused: React.Dispatch<React.SetStateAction<boolean>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useFormControl.d.ts","sourceRoot":"","sources":["../../../src/form-control/useFormControl.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useFormControl.d.ts","sourceRoot":"","sources":["../../../src/form-control/useFormControl.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -37,4 +37,112 @@ describe('mergeRefs', () => {
|
|
|
37
37
|
expect(callback).toHaveBeenCalledWith('shared');
|
|
38
38
|
expect(objectRef.current).toBe('shared');
|
|
39
39
|
});
|
|
40
|
+
|
|
41
|
+
describe('React 19 ref-cleanup semantics', () => {
|
|
42
|
+
it('returns a cleanup function when called with a non-null value', () => {
|
|
43
|
+
const ref = createRef<string>();
|
|
44
|
+
const merged = mergeRefs(ref);
|
|
45
|
+
|
|
46
|
+
const cleanup = merged('value');
|
|
47
|
+
|
|
48
|
+
expect(typeof cleanup).toBe('function');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('does not return a cleanup when called with null', () => {
|
|
52
|
+
const ref = createRef<string>();
|
|
53
|
+
const merged = mergeRefs(ref);
|
|
54
|
+
|
|
55
|
+
const result = merged(null);
|
|
56
|
+
|
|
57
|
+
expect(result).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('cleanup nulls object refs', () => {
|
|
61
|
+
const refA = createRef<string>();
|
|
62
|
+
const refB = createRef<string>();
|
|
63
|
+
const merged = mergeRefs(refA, refB);
|
|
64
|
+
|
|
65
|
+
const cleanup = merged('value') as () => void;
|
|
66
|
+
expect(refA.current).toBe('value');
|
|
67
|
+
expect(refB.current).toBe('value');
|
|
68
|
+
|
|
69
|
+
cleanup();
|
|
70
|
+
expect(refA.current).toBeNull();
|
|
71
|
+
expect(refB.current).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('cleanup calls callback refs with null when they did not return a cleanup', () => {
|
|
75
|
+
const callback = jest.fn();
|
|
76
|
+
const merged = mergeRefs(callback);
|
|
77
|
+
|
|
78
|
+
const cleanup = merged('value') as () => void;
|
|
79
|
+
expect(callback).toHaveBeenCalledWith('value');
|
|
80
|
+
|
|
81
|
+
callback.mockClear();
|
|
82
|
+
cleanup();
|
|
83
|
+
expect(callback).toHaveBeenCalledWith(null);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('cleanup invokes the inner cleanup instead of calling ref(null) for refs that returned one', () => {
|
|
87
|
+
const innerCleanup = jest.fn();
|
|
88
|
+
const callbackWithCleanup = jest.fn().mockReturnValue(innerCleanup);
|
|
89
|
+
const merged = mergeRefs(callbackWithCleanup);
|
|
90
|
+
|
|
91
|
+
const cleanup = merged('element') as () => void;
|
|
92
|
+
expect(callbackWithCleanup).toHaveBeenCalledWith('element');
|
|
93
|
+
|
|
94
|
+
callbackWithCleanup.mockClear();
|
|
95
|
+
cleanup();
|
|
96
|
+
expect(innerCleanup).toHaveBeenCalledTimes(1);
|
|
97
|
+
expect(callbackWithCleanup).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('handles mixed refs: object, callback with cleanup, callback without cleanup', () => {
|
|
101
|
+
const objectRef = createRef<string>();
|
|
102
|
+
const innerCleanup = jest.fn();
|
|
103
|
+
const callbackWithCleanup = jest.fn().mockReturnValue(innerCleanup);
|
|
104
|
+
const callbackNoCleanup = jest.fn();
|
|
105
|
+
|
|
106
|
+
const merged = mergeRefs(objectRef, callbackWithCleanup, callbackNoCleanup);
|
|
107
|
+
const cleanup = merged('node') as () => void;
|
|
108
|
+
|
|
109
|
+
expect(objectRef.current).toBe('node');
|
|
110
|
+
expect(callbackWithCleanup).toHaveBeenCalledWith('node');
|
|
111
|
+
expect(callbackNoCleanup).toHaveBeenCalledWith('node');
|
|
112
|
+
|
|
113
|
+
callbackWithCleanup.mockClear();
|
|
114
|
+
callbackNoCleanup.mockClear();
|
|
115
|
+
cleanup();
|
|
116
|
+
|
|
117
|
+
expect(objectRef.current).toBeNull();
|
|
118
|
+
expect(innerCleanup).toHaveBeenCalledTimes(1);
|
|
119
|
+
expect(callbackWithCleanup).not.toHaveBeenCalled();
|
|
120
|
+
expect(callbackNoCleanup).toHaveBeenCalledWith(null);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('null-call path still works for React 18 compat (calls all refs with null)', () => {
|
|
124
|
+
const objectRef = createRef<string>();
|
|
125
|
+
const callback = jest.fn();
|
|
126
|
+
const merged = mergeRefs(objectRef, callback);
|
|
127
|
+
|
|
128
|
+
merged('value');
|
|
129
|
+
callback.mockClear();
|
|
130
|
+
|
|
131
|
+
merged(null);
|
|
132
|
+
|
|
133
|
+
expect(objectRef.current).toBeNull();
|
|
134
|
+
expect(callback).toHaveBeenCalledWith(null);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('skips undefined entries in cleanup', () => {
|
|
138
|
+
const objectRef = createRef<string>();
|
|
139
|
+
const merged = mergeRefs(undefined, objectRef, undefined);
|
|
140
|
+
|
|
141
|
+
const cleanup = merged('value') as () => void;
|
|
142
|
+
expect(objectRef.current).toBe('value');
|
|
143
|
+
|
|
144
|
+
cleanup();
|
|
145
|
+
expect(objectRef.current).toBeNull();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
40
148
|
});
|
package/src/common/mergeRefs.ts
CHANGED
|
@@ -1,13 +1,56 @@
|
|
|
1
1
|
import type { Ref, RefCallback } from 'react';
|
|
2
2
|
|
|
3
|
+
type CleanupFn = () => void;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Merges multiple refs (object or callback) into a single callback ref.
|
|
7
|
+
*
|
|
8
|
+
* Supports React 19 ref-cleanup semantics: if an inner callback ref returns a
|
|
9
|
+
* cleanup function, that cleanup is captured and invoked when the merged ref is
|
|
10
|
+
* detached — either via the returned cleanup (React 19) or via a subsequent
|
|
11
|
+
* call with `null` (React 18 backward-compat path).
|
|
12
|
+
*/
|
|
3
13
|
export function mergeRefs<T>(...refs: (Ref<T> | undefined)[]): RefCallback<T> {
|
|
4
|
-
return (value: T | null) => {
|
|
14
|
+
return (value: T | null): CleanupFn | undefined => {
|
|
15
|
+
if (value === null) {
|
|
16
|
+
for (const ref of refs) {
|
|
17
|
+
if (typeof ref === 'function') {
|
|
18
|
+
ref(null);
|
|
19
|
+
} else if (ref != null) {
|
|
20
|
+
ref.current = null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const cleanups: (CleanupFn | undefined)[] = [];
|
|
5
27
|
for (const ref of refs) {
|
|
6
28
|
if (typeof ref === 'function') {
|
|
7
|
-
ref(value);
|
|
29
|
+
cleanups.push(ref(value) as CleanupFn | undefined);
|
|
8
30
|
} else if (ref != null) {
|
|
9
31
|
ref.current = value;
|
|
32
|
+
cleanups.push(undefined);
|
|
33
|
+
} else {
|
|
34
|
+
cleanups.push(undefined);
|
|
10
35
|
}
|
|
11
36
|
}
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
for (let i = 0; i < refs.length; i++) {
|
|
40
|
+
const ref = refs[i];
|
|
41
|
+
if (ref == null) continue;
|
|
42
|
+
|
|
43
|
+
if (typeof ref === 'function') {
|
|
44
|
+
const refCleanup = cleanups[i];
|
|
45
|
+
if (typeof refCleanup === 'function') {
|
|
46
|
+
refCleanup();
|
|
47
|
+
} else {
|
|
48
|
+
ref(null);
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
ref.current = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
12
55
|
};
|
|
13
56
|
}
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Platform
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
3
|
import { ariaAttr } from '../common/accessibilityUtils';
|
|
4
4
|
|
|
5
5
|
type HTMLProps = Record<string, unknown>;
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Minimal contract for any control registered on `FormControlContextValue.inputRef`.
|
|
9
|
+
*
|
|
10
|
+
* `Field.Label` / `focusInput()` only need to call `.focus()` on the registered node;
|
|
11
|
+
* narrower control-specific shapes (`TextInput`, `HTMLInputElement`, custom focus
|
|
12
|
+
* bridges like `Select.Trigger`'s) all satisfy this contract. Consumers that need
|
|
13
|
+
* more (e.g. reading `.value`) should cast locally — keeping the context type
|
|
14
|
+
* minimal avoids a typed lie when a non-input bridge is registered.
|
|
15
|
+
*/
|
|
16
|
+
export interface Focusable {
|
|
17
|
+
focus(): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
export interface FormControlState {
|
|
8
21
|
isRequired: boolean;
|
|
9
22
|
isInvalid: boolean;
|
|
@@ -25,8 +38,12 @@ export interface FormControlContextValue extends FormControlState {
|
|
|
25
38
|
/**
|
|
26
39
|
* @platform native — set by `Input.Field` / `Select.Trigger` for label press-to-focus.
|
|
27
40
|
* On web, use `<label htmlFor>` with a labelable control (`input`, `button`, `select`, …).
|
|
41
|
+
*
|
|
42
|
+
* Typed as `Focusable` so any control (TextInput, HTMLInputElement, custom focus
|
|
43
|
+
* bridge) can register without a cast at the assignment site. Consumers that need
|
|
44
|
+
* the narrower shape cast locally.
|
|
28
45
|
*/
|
|
29
|
-
inputRef?: React.RefObject<
|
|
46
|
+
inputRef?: React.RefObject<Focusable | null>;
|
|
30
47
|
focusInput?: () => void;
|
|
31
48
|
/** True when the primary child control is focused or (for Select) open — drives `Field.Label` styling. */
|
|
32
49
|
isLabelFocused?: boolean;
|
|
@@ -60,7 +77,7 @@ export interface FormControlRootReturn {
|
|
|
60
77
|
feedbackId: string;
|
|
61
78
|
helpTextId: string;
|
|
62
79
|
htmlProps: HTMLProps;
|
|
63
|
-
inputRef?: React.RefObject<
|
|
80
|
+
inputRef?: React.RefObject<Focusable | null>;
|
|
64
81
|
focusInput?: () => void;
|
|
65
82
|
isLabelFocused: boolean;
|
|
66
83
|
setIsLabelFocused: React.Dispatch<React.SetStateAction<boolean>>;
|
|
@@ -94,7 +111,7 @@ export function useFormControlRoot(props: FormControlRootProps): FormControlRoot
|
|
|
94
111
|
|
|
95
112
|
const [isLabelFocused, setIsLabelFocused] = React.useState(false);
|
|
96
113
|
|
|
97
|
-
const inputRef = React.useRef<
|
|
114
|
+
const inputRef = React.useRef<Focusable | null>(null);
|
|
98
115
|
|
|
99
116
|
const focusInput = React.useCallback(() => {
|
|
100
117
|
const node = inputRef.current;
|