@octanejs/base-ui 0.1.1
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/LICENSE +21 -0
- package/README.md +33 -0
- package/package.json +47 -0
- package/src/avatar.ts +308 -0
- package/src/checkbox-group.ts +183 -0
- package/src/checkbox.ts +578 -0
- package/src/field.ts +676 -0
- package/src/fieldset.ts +150 -0
- package/src/form.ts +183 -0
- package/src/index.ts +23 -0
- package/src/input.ts +9 -0
- package/src/internal.ts +42 -0
- package/src/merge-props.ts +2 -0
- package/src/meter.ts +204 -0
- package/src/number-field.ts +978 -0
- package/src/progress.ts +260 -0
- package/src/radio-group.ts +285 -0
- package/src/radio.ts +401 -0
- package/src/separator.ts +43 -0
- package/src/slider.ts +1617 -0
- package/src/switch.ts +374 -0
- package/src/toggle-group.ts +136 -0
- package/src/toggle.ts +145 -0
- package/src/use-render.ts +40 -0
- package/src/utils/CheckboxGroupContext.ts +30 -0
- package/src/utils/CompositeRootContext.ts +27 -0
- package/src/utils/DirectionContext.ts +15 -0
- package/src/utils/RadioGroupContext.ts +27 -0
- package/src/utils/ToggleGroupContext.ts +33 -0
- package/src/utils/addEventListener.ts +13 -0
- package/src/utils/areArraysEqual.ts +11 -0
- package/src/utils/clamp.ts +8 -0
- package/src/utils/composeRefs.ts +53 -0
- package/src/utils/composite/CompositeItem.ts +56 -0
- package/src/utils/composite/CompositeList.ts +190 -0
- package/src/utils/composite/CompositeListContext.ts +23 -0
- package/src/utils/composite/CompositeRoot.ts +132 -0
- package/src/utils/composite/keys.ts +179 -0
- package/src/utils/composite/list-utils.ts +78 -0
- package/src/utils/composite/useCompositeItem.ts +58 -0
- package/src/utils/composite/useCompositeListItem.ts +108 -0
- package/src/utils/composite/useCompositeRoot.ts +346 -0
- package/src/utils/contains.ts +22 -0
- package/src/utils/createChangeEventDetails.ts +81 -0
- package/src/utils/field/FieldItemContext.ts +13 -0
- package/src/utils/field/FieldRootContext.ts +98 -0
- package/src/utils/field/FormContext.ts +37 -0
- package/src/utils/field/LabelableContext.ts +32 -0
- package/src/utils/field/LabelableProvider.ts +108 -0
- package/src/utils/field/constants.ts +80 -0
- package/src/utils/field/getCombinedFieldValidityData.ts +16 -0
- package/src/utils/field/useAriaLabelledBy.ts +86 -0
- package/src/utils/field/useFieldControlRegistration.ts +179 -0
- package/src/utils/field/useFieldValidation.ts +311 -0
- package/src/utils/field/useLabel.ts +95 -0
- package/src/utils/field/useLabelableId.ts +99 -0
- package/src/utils/field/useRegisterFieldControl.ts +41 -0
- package/src/utils/formatNumber.ts +44 -0
- package/src/utils/getDefaultFormSubmitter.ts +19 -0
- package/src/utils/getElementRef.ts +13 -0
- package/src/utils/getStateAttributesProps.ts +34 -0
- package/src/utils/matchesFocusVisible.ts +16 -0
- package/src/utils/mergeObjects.ts +18 -0
- package/src/utils/mergeProps.ts +206 -0
- package/src/utils/noop.ts +4 -0
- package/src/utils/number/constants.ts +4 -0
- package/src/utils/number/parse.ts +227 -0
- package/src/utils/number/types.ts +25 -0
- package/src/utils/number/validate.ts +129 -0
- package/src/utils/owner.ts +9 -0
- package/src/utils/platform.ts +15 -0
- package/src/utils/resolveAriaLabelledBy.ts +11 -0
- package/src/utils/resolveClassName.ts +8 -0
- package/src/utils/resolveRef.ts +10 -0
- package/src/utils/resolveStyle.ts +10 -0
- package/src/utils/serializeValue.ts +15 -0
- package/src/utils/slider/asc.ts +4 -0
- package/src/utils/slider/getMidpoint.ts +14 -0
- package/src/utils/slider/getPushedThumbValues.ts +73 -0
- package/src/utils/slider/getSliderValue.ts +27 -0
- package/src/utils/slider/replaceArrayItemAtIndex.ts +8 -0
- package/src/utils/slider/resolveThumbCollision.ts +178 -0
- package/src/utils/slider/roundValueToStep.ts +22 -0
- package/src/utils/slider/validateMinimumDistance.ts +22 -0
- package/src/utils/slider/valueArrayToPercentages.ts +11 -0
- package/src/utils/stringifyLocale.ts +10 -0
- package/src/utils/useAnimationFrame.ts +59 -0
- package/src/utils/useAnimationsFinished.ts +100 -0
- package/src/utils/useBaseUiId.ts +19 -0
- package/src/utils/useButton.ts +217 -0
- package/src/utils/useCheckboxGroupParent.ts +113 -0
- package/src/utils/useControlled.ts +42 -0
- package/src/utils/useFocusableWhenDisabled.ts +84 -0
- package/src/utils/useForcedRerendering.ts +11 -0
- package/src/utils/useIsHydrating.ts +9 -0
- package/src/utils/useOpenChangeComplete.ts +46 -0
- package/src/utils/usePressAndHold.ts +35 -0
- package/src/utils/useRefWithInit.ts +23 -0
- package/src/utils/useRegisteredLabelId.ts +32 -0
- package/src/utils/useRenderElement.ts +165 -0
- package/src/utils/useStableCallback.ts +26 -0
- package/src/utils/useTimeout.ts +51 -0
- package/src/utils/useTransitionStatus.ts +115 -0
- package/src/utils/useValueAsRef.ts +25 -0
- package/src/utils/useValueChanged.ts +37 -0
- package/src/utils/valueToPercent.ts +4 -0
- package/src/utils/visuallyHidden.ts +24 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/internals/labelable-provider/LabelableProvider.tsx.
|
|
2
|
+
// Provides the REAL LabelableContext (control-id registry, labelId, message ids for
|
|
3
|
+
// aria-describedby) that Field parts read — overriding the inert default context.
|
|
4
|
+
import { createElement, useCallback, useContext, useMemo, useState } from 'octane';
|
|
5
|
+
|
|
6
|
+
import { S, subSlot } from '../../internal';
|
|
7
|
+
import { useBaseUiId } from '../useBaseUiId';
|
|
8
|
+
import { useRefWithInit } from '../useRefWithInit';
|
|
9
|
+
import { useStableCallback } from '../useStableCallback';
|
|
10
|
+
import { LabelableContext, type LabelableContextValue } from './LabelableContext';
|
|
11
|
+
|
|
12
|
+
export interface LabelableProviderProps {
|
|
13
|
+
controlId?: string | null;
|
|
14
|
+
labelId?: string;
|
|
15
|
+
children?: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function LabelableProvider(props: LabelableProviderProps): any {
|
|
19
|
+
const slot = S('LabelableProvider');
|
|
20
|
+
const defaultId = useBaseUiId(undefined, subSlot(slot, 'default'));
|
|
21
|
+
const initialControlId = props.controlId === undefined ? defaultId : props.controlId;
|
|
22
|
+
|
|
23
|
+
const [controlId, setControlIdState] = useState<string | null | undefined>(
|
|
24
|
+
initialControlId,
|
|
25
|
+
subSlot(slot, 'controlId'),
|
|
26
|
+
);
|
|
27
|
+
const [labelId, setLabelId] = useState<string | undefined>(
|
|
28
|
+
props.labelId,
|
|
29
|
+
subSlot(slot, 'labelId'),
|
|
30
|
+
);
|
|
31
|
+
const [messageIds, setMessageIds] = useState<string[]>([], subSlot(slot, 'messageIds'));
|
|
32
|
+
|
|
33
|
+
const registrationsRef = useRefWithInit<Map<symbol, string | null>>(
|
|
34
|
+
() => new Map(),
|
|
35
|
+
subSlot(slot, 'reg'),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const { messageIds: parentMessageIds } = useContext(LabelableContext);
|
|
39
|
+
|
|
40
|
+
const registerControlId = useStableCallback(
|
|
41
|
+
(source: symbol, nextId: string | null | undefined) => {
|
|
42
|
+
const registrations = registrationsRef.current;
|
|
43
|
+
if (nextId === undefined) {
|
|
44
|
+
registrations.delete(source);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
registrations.set(source, nextId);
|
|
48
|
+
setControlIdState((prev: string | null | undefined) => {
|
|
49
|
+
if (registrations.size === 0) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
let nextControlId: string | null | undefined;
|
|
53
|
+
for (const id of registrations.values()) {
|
|
54
|
+
if (prev !== undefined && id === prev) {
|
|
55
|
+
return prev;
|
|
56
|
+
}
|
|
57
|
+
if (nextControlId === undefined) {
|
|
58
|
+
nextControlId = id;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return nextControlId;
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
subSlot(slot, 'registerControlId'),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const getDescriptionProps = useCallback(
|
|
68
|
+
(externalProps: Record<string, any>) => {
|
|
69
|
+
const ids = externalProps['aria-describedby']
|
|
70
|
+
? externalProps['aria-describedby'].split(' ')
|
|
71
|
+
: [];
|
|
72
|
+
ids.push(...parentMessageIds, ...messageIds);
|
|
73
|
+
return {
|
|
74
|
+
...externalProps,
|
|
75
|
+
'aria-describedby': Array.from(new Set(ids)).join(' ') || undefined,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
[parentMessageIds, messageIds],
|
|
79
|
+
subSlot(slot, 'getDesc'),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const contextValue: LabelableContextValue = useMemo(
|
|
83
|
+
() => ({
|
|
84
|
+
controlId,
|
|
85
|
+
registerControlId,
|
|
86
|
+
labelId,
|
|
87
|
+
setLabelId,
|
|
88
|
+
messageIds,
|
|
89
|
+
setMessageIds,
|
|
90
|
+
getDescriptionProps,
|
|
91
|
+
}),
|
|
92
|
+
[
|
|
93
|
+
controlId,
|
|
94
|
+
registerControlId,
|
|
95
|
+
labelId,
|
|
96
|
+
setLabelId,
|
|
97
|
+
messageIds,
|
|
98
|
+
setMessageIds,
|
|
99
|
+
getDescriptionProps,
|
|
100
|
+
],
|
|
101
|
+
subSlot(slot, 'ctx'),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return createElement(LabelableContext.Provider, {
|
|
105
|
+
value: contextValue,
|
|
106
|
+
children: props.children,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/internals/field-constants/constants.ts and the
|
|
2
|
+
// FieldRootState/FieldValidityData types (field/root/FieldRoot). The default field state +
|
|
3
|
+
// validity, plus the shared `fieldValidityMapping` (valid → data-valid / data-invalid; null →
|
|
4
|
+
// no attribute) reused by every field-aware control's stateAttributesMapping.
|
|
5
|
+
import type { StateAttributesMapping } from '../getStateAttributesProps';
|
|
6
|
+
|
|
7
|
+
export type FormValidationMode = 'onSubmit' | 'onBlur' | 'onChange';
|
|
8
|
+
|
|
9
|
+
export interface FieldRootState {
|
|
10
|
+
disabled: boolean;
|
|
11
|
+
touched: boolean;
|
|
12
|
+
dirty: boolean;
|
|
13
|
+
filled: boolean;
|
|
14
|
+
focused: boolean;
|
|
15
|
+
valid: boolean | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FieldValidityState {
|
|
19
|
+
badInput: boolean;
|
|
20
|
+
customError: boolean;
|
|
21
|
+
patternMismatch: boolean;
|
|
22
|
+
rangeOverflow: boolean;
|
|
23
|
+
rangeUnderflow: boolean;
|
|
24
|
+
stepMismatch: boolean;
|
|
25
|
+
tooLong: boolean;
|
|
26
|
+
tooShort: boolean;
|
|
27
|
+
typeMismatch: boolean;
|
|
28
|
+
valid: boolean | null;
|
|
29
|
+
valueMissing: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface FieldValidityData {
|
|
33
|
+
state: FieldValidityState;
|
|
34
|
+
errors: string[];
|
|
35
|
+
error: string;
|
|
36
|
+
value: unknown;
|
|
37
|
+
initialValue: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const DEFAULT_VALIDITY_STATE: FieldValidityState = {
|
|
41
|
+
badInput: false,
|
|
42
|
+
customError: false,
|
|
43
|
+
patternMismatch: false,
|
|
44
|
+
rangeOverflow: false,
|
|
45
|
+
rangeUnderflow: false,
|
|
46
|
+
stepMismatch: false,
|
|
47
|
+
tooLong: false,
|
|
48
|
+
tooShort: false,
|
|
49
|
+
typeMismatch: false,
|
|
50
|
+
valid: null,
|
|
51
|
+
valueMissing: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const DEFAULT_FIELD_STATE_ATTRIBUTES: Pick<
|
|
55
|
+
FieldRootState,
|
|
56
|
+
'valid' | 'touched' | 'dirty' | 'filled' | 'focused'
|
|
57
|
+
> = {
|
|
58
|
+
valid: null,
|
|
59
|
+
touched: false,
|
|
60
|
+
dirty: false,
|
|
61
|
+
filled: false,
|
|
62
|
+
focused: false,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const DEFAULT_FIELD_ROOT_STATE: FieldRootState = {
|
|
66
|
+
disabled: false,
|
|
67
|
+
...DEFAULT_FIELD_STATE_ATTRIBUTES,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const fieldValidityMapping: StateAttributesMapping<{ valid: boolean | null }> = {
|
|
71
|
+
valid(value: boolean | null): Record<string, string> | null {
|
|
72
|
+
if (value === null) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (value) {
|
|
76
|
+
return { 'data-valid': '' };
|
|
77
|
+
}
|
|
78
|
+
return { 'data-invalid': '' };
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/field/utils/getCombinedFieldValidityData.ts.
|
|
2
|
+
// Folds the external `invalid` flag into the field's stateful validity.
|
|
3
|
+
import type { FieldValidityData } from './constants';
|
|
4
|
+
|
|
5
|
+
export function getCombinedFieldValidityData(
|
|
6
|
+
validityData: FieldValidityData,
|
|
7
|
+
invalid: boolean | undefined,
|
|
8
|
+
): FieldValidityData {
|
|
9
|
+
return {
|
|
10
|
+
...validityData,
|
|
11
|
+
state: {
|
|
12
|
+
...validityData.state,
|
|
13
|
+
valid: !invalid && validityData.state.valid,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/internals/labelable-provider/useAriaLabelledBy.ts.
|
|
2
|
+
// Resolves a control's `aria-labelledby`: an explicit value, else the Field label id, else a
|
|
3
|
+
// fallback discovered from a wrapping/sibling native <label>. The effect runs after every
|
|
4
|
+
// commit so DOM label association changes are reflected.
|
|
5
|
+
//
|
|
6
|
+
// SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
7
|
+
import { useLayoutEffect, useState } from 'octane';
|
|
8
|
+
|
|
9
|
+
import { S, splitSlot, subSlot } from '../../internal';
|
|
10
|
+
import { useBaseUiId } from '../useBaseUiId';
|
|
11
|
+
|
|
12
|
+
type LabelSource = HTMLElement & { labels?: NodeListOf<HTMLLabelElement> | null };
|
|
13
|
+
|
|
14
|
+
export function useAriaLabelledBy(...args: any[]): string | undefined {
|
|
15
|
+
const [user, slotArg] = splitSlot(args);
|
|
16
|
+
const slot = slotArg ?? S('useAriaLabelledBy');
|
|
17
|
+
const explicitAriaLabelledBy = user[0] as string | undefined;
|
|
18
|
+
const labelId = user[1] as string | undefined;
|
|
19
|
+
const labelSourceRef = user[2] as { current: LabelSource | null };
|
|
20
|
+
const enableFallback = (user[3] as boolean | undefined) ?? true;
|
|
21
|
+
const labelSourceId = user[4] as string | undefined;
|
|
22
|
+
|
|
23
|
+
const [fallbackAriaLabelledBy, setFallbackAriaLabelledBy] = useState<string | undefined>(
|
|
24
|
+
undefined,
|
|
25
|
+
subSlot(slot, 'fb'),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const generatedLabelId = useBaseUiId(
|
|
29
|
+
labelSourceId ? `${labelSourceId}-label` : undefined,
|
|
30
|
+
subSlot(slot, 'gen'),
|
|
31
|
+
);
|
|
32
|
+
const ariaLabelledBy = explicitAriaLabelledBy ?? labelId ?? fallbackAriaLabelledBy;
|
|
33
|
+
|
|
34
|
+
// No deps → runs after every commit (DOM label association can change without prop/state
|
|
35
|
+
// deps changing).
|
|
36
|
+
useLayoutEffect(
|
|
37
|
+
() => {
|
|
38
|
+
const nextAriaLabelledBy =
|
|
39
|
+
explicitAriaLabelledBy || labelId || !enableFallback
|
|
40
|
+
? undefined
|
|
41
|
+
: getAriaLabelledBy(labelSourceRef.current, generatedLabelId);
|
|
42
|
+
if (fallbackAriaLabelledBy !== nextAriaLabelledBy) {
|
|
43
|
+
setFallbackAriaLabelledBy(nextAriaLabelledBy);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
undefined,
|
|
47
|
+
subSlot(slot, 'e:fallback'),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return ariaLabelledBy;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getAriaLabelledBy(
|
|
54
|
+
labelSource: LabelSource | null | undefined,
|
|
55
|
+
generatedLabelId: string | undefined,
|
|
56
|
+
): string | undefined {
|
|
57
|
+
const label = findAssociatedLabel(labelSource);
|
|
58
|
+
if (!label) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
if (!label.id && generatedLabelId) {
|
|
62
|
+
label.id = generatedLabelId;
|
|
63
|
+
}
|
|
64
|
+
return label.id || undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function findAssociatedLabel(
|
|
68
|
+
labelSource: LabelSource | null | undefined,
|
|
69
|
+
): HTMLLabelElement | undefined {
|
|
70
|
+
if (!labelSource) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const parent = labelSource.parentElement;
|
|
74
|
+
if (parent && parent.tagName === 'LABEL') {
|
|
75
|
+
return parent as HTMLLabelElement;
|
|
76
|
+
}
|
|
77
|
+
const controlId = labelSource.id;
|
|
78
|
+
if (controlId) {
|
|
79
|
+
const nextSibling = labelSource.nextElementSibling as HTMLLabelElement | null;
|
|
80
|
+
if (nextSibling && nextSibling.htmlFor === controlId) {
|
|
81
|
+
return nextSibling;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const labels = labelSource.labels;
|
|
85
|
+
return labels ? (labels[0] as HTMLLabelElement | undefined) : undefined;
|
|
86
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/internals/field-register-control/useFieldControlRegistration.ts.
|
|
2
|
+
// Registers the field's control with the enclosing <Form> (so submit-time validation + focus
|
|
3
|
+
// work) and exposes an imperative `validate`.
|
|
4
|
+
//
|
|
5
|
+
// SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
6
|
+
import { useLayoutEffect, useRef } from 'octane';
|
|
7
|
+
|
|
8
|
+
import { S, splitSlot, subSlot } from '../../internal';
|
|
9
|
+
import { getCombinedFieldValidityData } from './getCombinedFieldValidityData';
|
|
10
|
+
import { useFormContext } from './FormContext';
|
|
11
|
+
import { useStableCallback } from '../useStableCallback';
|
|
12
|
+
import type { FieldValidityData } from './constants';
|
|
13
|
+
|
|
14
|
+
export interface FieldControlRegistration {
|
|
15
|
+
controlRef: { current: any };
|
|
16
|
+
id: string | undefined;
|
|
17
|
+
name?: string;
|
|
18
|
+
getValue?: () => unknown;
|
|
19
|
+
value: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UseFieldControlRegistrationParameters {
|
|
23
|
+
commit: (value: unknown) => void;
|
|
24
|
+
invalid: boolean;
|
|
25
|
+
markedDirtyRef: { current: boolean };
|
|
26
|
+
name: string | undefined;
|
|
27
|
+
setRegisteredFieldName: (name: string | undefined) => void;
|
|
28
|
+
setRegisteredFieldId: (id: string | undefined) => void;
|
|
29
|
+
setValidityData: (
|
|
30
|
+
next: FieldValidityData | ((prev: FieldValidityData) => FieldValidityData),
|
|
31
|
+
) => void;
|
|
32
|
+
validityData: FieldValidityData;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function useFieldControlRegistration(...args: any[]): readonly [() => void, any] {
|
|
36
|
+
const [user, slotArg] = splitSlot(args);
|
|
37
|
+
const slot = slotArg ?? S('useFieldControlRegistration');
|
|
38
|
+
const {
|
|
39
|
+
commit,
|
|
40
|
+
invalid,
|
|
41
|
+
markedDirtyRef,
|
|
42
|
+
name,
|
|
43
|
+
setRegisteredFieldName,
|
|
44
|
+
setRegisteredFieldId,
|
|
45
|
+
setValidityData,
|
|
46
|
+
validityData,
|
|
47
|
+
} = user[0] as UseFieldControlRegistrationParameters;
|
|
48
|
+
|
|
49
|
+
const { formRef } = useFormContext();
|
|
50
|
+
|
|
51
|
+
const activeFieldControlSourceRef = useRef<symbol | null>(null, subSlot(slot, 'active'));
|
|
52
|
+
const registrationRef = useRef<FieldControlRegistration | null>(null, subSlot(slot, 'reg'));
|
|
53
|
+
const fallbackControlRef = useRef<any>(null, subSlot(slot, 'fallback'));
|
|
54
|
+
|
|
55
|
+
const getValueForForm = useStableCallback(
|
|
56
|
+
() => {
|
|
57
|
+
const registration = registrationRef.current;
|
|
58
|
+
if (!registration) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
if (registration.getValue) {
|
|
62
|
+
return registration.getValue();
|
|
63
|
+
}
|
|
64
|
+
return registration.value;
|
|
65
|
+
},
|
|
66
|
+
subSlot(slot, 'getValue'),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
function getRegistrationValue(registration: FieldControlRegistration): unknown {
|
|
70
|
+
return registration.value === undefined ? getValueForForm() : registration.value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const validate = useStableCallback(
|
|
74
|
+
() => {
|
|
75
|
+
const registration = registrationRef.current;
|
|
76
|
+
markedDirtyRef.current = true;
|
|
77
|
+
if (!registration) {
|
|
78
|
+
commit(validityData.value);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
commit(getRegistrationValue(registration));
|
|
82
|
+
},
|
|
83
|
+
subSlot(slot, 'validate'),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
function refreshRegistration(): void {
|
|
87
|
+
const registration = registrationRef.current;
|
|
88
|
+
if (!registration || !registration.id) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
formRef.current.fields.set(registration.id, {
|
|
92
|
+
getValue: getValueForForm,
|
|
93
|
+
name: name ?? registration.name,
|
|
94
|
+
controlRef: registration.controlRef ?? fallbackControlRef,
|
|
95
|
+
validityData: getCombinedFieldValidityData(validityData, invalid),
|
|
96
|
+
validate,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function deleteRegistration(id = registrationRef.current?.id): void {
|
|
101
|
+
if (id) {
|
|
102
|
+
formRef.current.fields.delete(id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function syncInitialValue(): void {
|
|
107
|
+
const registration = registrationRef.current;
|
|
108
|
+
if (!registration) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const initialValue = getRegistrationValue(registration);
|
|
112
|
+
if (validityData.initialValue === null && initialValue !== null) {
|
|
113
|
+
setValidityData((prev) => ({ ...prev, initialValue }));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
useLayoutEffect(
|
|
118
|
+
() => {
|
|
119
|
+
const registration = registrationRef.current;
|
|
120
|
+
if (!registration || !registration.id) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
setRegisteredFieldName(name ? undefined : registration.name);
|
|
124
|
+
formRef.current.fields.set(registration.id, {
|
|
125
|
+
getValue: getValueForForm,
|
|
126
|
+
name: name ?? registration.name,
|
|
127
|
+
controlRef: registration.controlRef ?? fallbackControlRef,
|
|
128
|
+
validityData: getCombinedFieldValidityData(validityData, invalid),
|
|
129
|
+
validate,
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
[formRef, getValueForForm, invalid, name, setRegisteredFieldName, validate, validityData],
|
|
133
|
+
subSlot(slot, 'e:register'),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
useLayoutEffect(
|
|
137
|
+
() => {
|
|
138
|
+
const fields = formRef.current.fields;
|
|
139
|
+
return () => {
|
|
140
|
+
const id = registrationRef.current?.id;
|
|
141
|
+
if (id) {
|
|
142
|
+
fields.delete(id);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
[formRef],
|
|
147
|
+
subSlot(slot, 'e:cleanup'),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const register = useStableCallback(
|
|
151
|
+
(source: symbol, registration: FieldControlRegistration | undefined) => {
|
|
152
|
+
if (!registration) {
|
|
153
|
+
if (activeFieldControlSourceRef.current === source) {
|
|
154
|
+
activeFieldControlSourceRef.current = null;
|
|
155
|
+
deleteRegistration();
|
|
156
|
+
registrationRef.current = null;
|
|
157
|
+
setRegisteredFieldName(undefined);
|
|
158
|
+
setRegisteredFieldId(undefined);
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const previousId = registrationRef.current?.id;
|
|
163
|
+
activeFieldControlSourceRef.current = source;
|
|
164
|
+
registrationRef.current = registration;
|
|
165
|
+
if (!name) {
|
|
166
|
+
setRegisteredFieldName(registration.name);
|
|
167
|
+
}
|
|
168
|
+
setRegisteredFieldId(registration.id);
|
|
169
|
+
if (previousId && previousId !== registration.id) {
|
|
170
|
+
deleteRegistration(previousId);
|
|
171
|
+
}
|
|
172
|
+
syncInitialValue();
|
|
173
|
+
refreshRegistration();
|
|
174
|
+
},
|
|
175
|
+
subSlot(slot, 'register'),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return [validate, register] as const;
|
|
179
|
+
}
|