@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,14 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/slider/utils/getMidpoint.ts. The `Coords`
|
|
2
|
+
// type (`@floating-ui/utils`) is inlined.
|
|
3
|
+
export interface Coords {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getMidpoint(element: Element): Coords {
|
|
9
|
+
const rect = element.getBoundingClientRect();
|
|
10
|
+
return {
|
|
11
|
+
x: (rect.left + rect.right) / 2,
|
|
12
|
+
y: (rect.top + rect.bottom) / 2,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/slider/utils/getPushedThumbValues.ts.
|
|
2
|
+
import { clamp } from '../clamp';
|
|
3
|
+
|
|
4
|
+
interface GetPushedThumbValuesParams {
|
|
5
|
+
values: readonly number[];
|
|
6
|
+
index: number;
|
|
7
|
+
nextValue: number;
|
|
8
|
+
min: number;
|
|
9
|
+
max: number;
|
|
10
|
+
step: number;
|
|
11
|
+
minStepsBetweenValues: number;
|
|
12
|
+
initialValues?: readonly number[] | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns a new array of slider values where attempting to move the thumb at `index`
|
|
17
|
+
* beyond its neighbours "pushes" them while respecting `minStepsBetweenValues`.
|
|
18
|
+
*/
|
|
19
|
+
export function getPushedThumbValues({
|
|
20
|
+
values,
|
|
21
|
+
index,
|
|
22
|
+
nextValue,
|
|
23
|
+
min,
|
|
24
|
+
max,
|
|
25
|
+
step,
|
|
26
|
+
minStepsBetweenValues,
|
|
27
|
+
initialValues,
|
|
28
|
+
}: GetPushedThumbValuesParams): number[] {
|
|
29
|
+
if (values.length === 0) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const nextValues = values.slice();
|
|
34
|
+
const minValueDifference = step * minStepsBetweenValues;
|
|
35
|
+
const lastIndex = nextValues.length - 1;
|
|
36
|
+
const baseInitialValues = initialValues ?? values;
|
|
37
|
+
|
|
38
|
+
const indexMin = min + index * minValueDifference;
|
|
39
|
+
const indexMax = max - (lastIndex - index) * minValueDifference;
|
|
40
|
+
nextValues[index] = clamp(nextValue, indexMin, indexMax);
|
|
41
|
+
|
|
42
|
+
for (let i = index + 1; i <= lastIndex; i += 1) {
|
|
43
|
+
const minAllowed = nextValues[i - 1] + minValueDifference;
|
|
44
|
+
const maxAllowed = max - (lastIndex - i) * minValueDifference;
|
|
45
|
+
const initialValue = baseInitialValues[i] ?? nextValues[i];
|
|
46
|
+
let candidate = Math.max(nextValues[i], minAllowed);
|
|
47
|
+
|
|
48
|
+
if (initialValue < candidate) {
|
|
49
|
+
candidate = Math.max(initialValue, minAllowed);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
nextValues[i] = clamp(candidate, minAllowed, maxAllowed);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (let i = index - 1; i >= 0; i -= 1) {
|
|
56
|
+
const maxAllowed = nextValues[i + 1] - minValueDifference;
|
|
57
|
+
const minAllowed = min + i * minValueDifference;
|
|
58
|
+
const initialValue = baseInitialValues[i] ?? nextValues[i];
|
|
59
|
+
let candidate = Math.min(nextValues[i], maxAllowed);
|
|
60
|
+
|
|
61
|
+
if (initialValue > candidate) {
|
|
62
|
+
candidate = Math.min(initialValue, maxAllowed);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
nextValues[i] = clamp(candidate, minAllowed, maxAllowed);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i <= lastIndex; i += 1) {
|
|
69
|
+
nextValues[i] = Number(nextValues[i].toFixed(12));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return nextValues;
|
|
73
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/slider/utils/getSliderValue.ts.
|
|
2
|
+
import { clamp } from '../clamp';
|
|
3
|
+
import { replaceArrayItemAtIndex } from './replaceArrayItemAtIndex';
|
|
4
|
+
|
|
5
|
+
export function getSliderValue(
|
|
6
|
+
valueInput: number,
|
|
7
|
+
index: number,
|
|
8
|
+
min: number,
|
|
9
|
+
max: number,
|
|
10
|
+
range: boolean,
|
|
11
|
+
values: readonly number[],
|
|
12
|
+
) {
|
|
13
|
+
let newValue: number | number[] = valueInput;
|
|
14
|
+
|
|
15
|
+
newValue = clamp(newValue, min, max);
|
|
16
|
+
|
|
17
|
+
if (range) {
|
|
18
|
+
newValue = replaceArrayItemAtIndex(
|
|
19
|
+
values,
|
|
20
|
+
index,
|
|
21
|
+
// Bound the new value to the thumb's neighbours.
|
|
22
|
+
clamp(newValue, values[index - 1] ?? -Infinity, values[index + 1] ?? Infinity),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return newValue;
|
|
27
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/slider/utils/replaceArrayItemAtIndex.ts.
|
|
2
|
+
import { asc } from './asc';
|
|
3
|
+
|
|
4
|
+
export function replaceArrayItemAtIndex(array: readonly number[], index: number, newValue: number) {
|
|
5
|
+
const output = array.slice();
|
|
6
|
+
output[index] = newValue;
|
|
7
|
+
return output.sort(asc);
|
|
8
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/slider/utils/resolveThumbCollision.ts. The
|
|
2
|
+
// `behavior` type (`SliderRootContext['thumbCollisionBehavior']`) is inlined to avoid a cycle.
|
|
3
|
+
import { clamp } from '../clamp';
|
|
4
|
+
import { getPushedThumbValues } from './getPushedThumbValues';
|
|
5
|
+
|
|
6
|
+
export type ThumbCollisionBehavior = 'push' | 'swap' | 'none';
|
|
7
|
+
|
|
8
|
+
export interface ResolveThumbCollisionParams {
|
|
9
|
+
behavior: ThumbCollisionBehavior;
|
|
10
|
+
values: readonly number[];
|
|
11
|
+
currentValues?: readonly number[] | null | undefined;
|
|
12
|
+
initialValues?: readonly number[] | null | undefined;
|
|
13
|
+
pressedIndex: number;
|
|
14
|
+
nextValue: number;
|
|
15
|
+
min: number;
|
|
16
|
+
max: number;
|
|
17
|
+
step: number;
|
|
18
|
+
minStepsBetweenValues: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ResolveThumbCollisionResult {
|
|
22
|
+
value: number | number[];
|
|
23
|
+
thumbIndex: number;
|
|
24
|
+
didSwap: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveThumbCollision({
|
|
28
|
+
behavior,
|
|
29
|
+
values,
|
|
30
|
+
currentValues,
|
|
31
|
+
initialValues,
|
|
32
|
+
pressedIndex,
|
|
33
|
+
nextValue,
|
|
34
|
+
min,
|
|
35
|
+
max,
|
|
36
|
+
step,
|
|
37
|
+
minStepsBetweenValues,
|
|
38
|
+
}: ResolveThumbCollisionParams): ResolveThumbCollisionResult {
|
|
39
|
+
const activeValues = currentValues ?? values;
|
|
40
|
+
const baselineValues = initialValues ?? values;
|
|
41
|
+
const range = activeValues.length > 1;
|
|
42
|
+
|
|
43
|
+
if (!range) {
|
|
44
|
+
return {
|
|
45
|
+
value: nextValue,
|
|
46
|
+
thumbIndex: 0,
|
|
47
|
+
didSwap: false,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const minValueDifference = step * minStepsBetweenValues;
|
|
52
|
+
|
|
53
|
+
switch (behavior) {
|
|
54
|
+
case 'swap': {
|
|
55
|
+
const pressedInitialValue = activeValues[pressedIndex];
|
|
56
|
+
const epsilon = 1e-7;
|
|
57
|
+
const candidateValues = activeValues.slice();
|
|
58
|
+
const previousNeighbor = candidateValues[pressedIndex - 1];
|
|
59
|
+
const nextNeighbor = candidateValues[pressedIndex + 1];
|
|
60
|
+
|
|
61
|
+
const lowerBound = previousNeighbor != null ? previousNeighbor + minValueDifference : min;
|
|
62
|
+
const upperBound = nextNeighbor != null ? nextNeighbor - minValueDifference : max;
|
|
63
|
+
|
|
64
|
+
const constrainedValue = clamp(nextValue, lowerBound, upperBound);
|
|
65
|
+
const pressedValueAfterClamp = Number(constrainedValue.toFixed(12));
|
|
66
|
+
candidateValues[pressedIndex] = pressedValueAfterClamp;
|
|
67
|
+
|
|
68
|
+
const movingForward = nextValue > pressedInitialValue;
|
|
69
|
+
const movingBackward = nextValue < pressedInitialValue;
|
|
70
|
+
|
|
71
|
+
const shouldSwapForward =
|
|
72
|
+
movingForward && nextNeighbor != null && nextValue >= nextNeighbor - epsilon;
|
|
73
|
+
const shouldSwapBackward =
|
|
74
|
+
movingBackward && previousNeighbor != null && nextValue <= previousNeighbor + epsilon;
|
|
75
|
+
|
|
76
|
+
if (!shouldSwapForward && !shouldSwapBackward) {
|
|
77
|
+
return {
|
|
78
|
+
value: candidateValues,
|
|
79
|
+
thumbIndex: pressedIndex,
|
|
80
|
+
didSwap: false,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const targetIndex = shouldSwapForward ? pressedIndex + 1 : pressedIndex - 1;
|
|
85
|
+
|
|
86
|
+
const initialValuesForPush = candidateValues.map((_, index) => {
|
|
87
|
+
if (index === pressedIndex) {
|
|
88
|
+
return pressedValueAfterClamp;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const baseline = baselineValues[index];
|
|
92
|
+
if (baseline != null) {
|
|
93
|
+
return baseline;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return activeValues[index];
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
let nextValueForTarget = nextValue;
|
|
100
|
+
if (shouldSwapForward) {
|
|
101
|
+
nextValueForTarget = Math.max(nextValue, candidateValues[targetIndex]);
|
|
102
|
+
} else {
|
|
103
|
+
nextValueForTarget = Math.min(nextValue, candidateValues[targetIndex]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const adjustedValues = getPushedThumbValues({
|
|
107
|
+
values: candidateValues,
|
|
108
|
+
index: targetIndex,
|
|
109
|
+
nextValue: nextValueForTarget,
|
|
110
|
+
min,
|
|
111
|
+
max,
|
|
112
|
+
step,
|
|
113
|
+
minStepsBetweenValues,
|
|
114
|
+
initialValues: initialValuesForPush,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const neighborIndex = shouldSwapForward ? targetIndex - 1 : targetIndex + 1;
|
|
118
|
+
|
|
119
|
+
if (neighborIndex >= 0 && neighborIndex < adjustedValues.length) {
|
|
120
|
+
const previousValue = adjustedValues[neighborIndex - 1];
|
|
121
|
+
const nextValueAfter = adjustedValues[neighborIndex + 1];
|
|
122
|
+
|
|
123
|
+
let neighborLowerBound = previousValue != null ? previousValue + minValueDifference : min;
|
|
124
|
+
neighborLowerBound = Math.max(neighborLowerBound, min + neighborIndex * minValueDifference);
|
|
125
|
+
|
|
126
|
+
let neighborUpperBound = nextValueAfter != null ? nextValueAfter - minValueDifference : max;
|
|
127
|
+
neighborUpperBound = Math.min(
|
|
128
|
+
neighborUpperBound,
|
|
129
|
+
max - (adjustedValues.length - 1 - neighborIndex) * minValueDifference,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const restoredValue = clamp(pressedValueAfterClamp, neighborLowerBound, neighborUpperBound);
|
|
133
|
+
adjustedValues[neighborIndex] = Number(restoredValue.toFixed(12));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
value: adjustedValues,
|
|
138
|
+
thumbIndex: targetIndex,
|
|
139
|
+
didSwap: true,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
case 'push': {
|
|
143
|
+
const nextValues = getPushedThumbValues({
|
|
144
|
+
values: activeValues,
|
|
145
|
+
index: pressedIndex,
|
|
146
|
+
nextValue,
|
|
147
|
+
min,
|
|
148
|
+
max,
|
|
149
|
+
step,
|
|
150
|
+
minStepsBetweenValues,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
value: nextValues,
|
|
155
|
+
thumbIndex: pressedIndex,
|
|
156
|
+
didSwap: false,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
case 'none':
|
|
160
|
+
default: {
|
|
161
|
+
const candidateValues = activeValues.slice();
|
|
162
|
+
const previousNeighbor = candidateValues[pressedIndex - 1];
|
|
163
|
+
const nextNeighbor = candidateValues[pressedIndex + 1];
|
|
164
|
+
|
|
165
|
+
const lowerBound = previousNeighbor != null ? previousNeighbor + minValueDifference : min;
|
|
166
|
+
const upperBound = nextNeighbor != null ? nextNeighbor - minValueDifference : max;
|
|
167
|
+
|
|
168
|
+
const constrainedValue = clamp(nextValue, lowerBound, upperBound);
|
|
169
|
+
candidateValues[pressedIndex] = Number(constrainedValue.toFixed(12));
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
value: candidateValues,
|
|
173
|
+
thumbIndex: pressedIndex,
|
|
174
|
+
didSwap: false,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/slider/utils/roundValueToStep.ts.
|
|
2
|
+
export function getDecimalPrecision(num: number) {
|
|
3
|
+
if (num === 0) {
|
|
4
|
+
return 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// This handles the case when num is very small (0.00000001), js will turn this into 1e-8.
|
|
8
|
+
// When num is bigger than 1 or less than -1 it won't get converted to this notation so it's fine.
|
|
9
|
+
if (Math.abs(num) < 1) {
|
|
10
|
+
const parts = num.toExponential().split('e-');
|
|
11
|
+
const matissaDecimalPart = parts[0].split('.')[1];
|
|
12
|
+
return (matissaDecimalPart ? matissaDecimalPart.length : 0) + parseInt(parts[1], 10);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const decimalPart = num.toString().split('.')[1];
|
|
16
|
+
return decimalPart ? decimalPart.length : 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function roundValueToStep(value: number, step: number, min: number) {
|
|
20
|
+
const nearest = Math.round((value - min) / step) * step + min;
|
|
21
|
+
return Number(nearest.toFixed(Math.max(getDecimalPrecision(step), getDecimalPrecision(min))));
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/slider/utils/validateMinimumDistance.ts.
|
|
2
|
+
export function validateMinimumDistance(
|
|
3
|
+
values: number | readonly number[],
|
|
4
|
+
step: number,
|
|
5
|
+
minStepsBetweenValues: number,
|
|
6
|
+
) {
|
|
7
|
+
if (!Array.isArray(values)) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const distances = values.reduce((acc: number[], val, index, vals) => {
|
|
12
|
+
if (index === vals.length - 1) {
|
|
13
|
+
return acc;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
acc.push(Math.abs(val - vals[index + 1]));
|
|
17
|
+
|
|
18
|
+
return acc;
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
return Math.min(...distances) >= step * minStepsBetweenValues;
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/slider/utils/valueArrayToPercentages.ts.
|
|
2
|
+
import { clamp } from '../clamp';
|
|
3
|
+
import { valueToPercent } from '../valueToPercent';
|
|
4
|
+
|
|
5
|
+
export function valueArrayToPercentages(values: number[], min: number, max: number) {
|
|
6
|
+
const output = [];
|
|
7
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
8
|
+
output.push(clamp(valueToPercent(values[i], min, max), 0, 100));
|
|
9
|
+
}
|
|
10
|
+
return output;
|
|
11
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/utils/stringifyLocale.ts.
|
|
2
|
+
export function stringifyLocale(locale?: Intl.LocalesArgument): string {
|
|
3
|
+
if (Array.isArray(locale)) {
|
|
4
|
+
return locale.map((value) => stringifyLocale(value)).join(',');
|
|
5
|
+
}
|
|
6
|
+
if (locale == null) {
|
|
7
|
+
return '';
|
|
8
|
+
}
|
|
9
|
+
return String(locale);
|
|
10
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/utils/src/useAnimationFrame.ts. `AnimationFrame` is a
|
|
2
|
+
// module-level rAF scheduler (request returns an id; cancel takes it); `useAnimationFrame`
|
|
3
|
+
// is a per-instance frame that cancels its pending callback on unmount. Base UI's Scheduler
|
|
4
|
+
// batches many callbacks into one rAF for perf — the observable behavior (run-next-frame,
|
|
5
|
+
// cancelable) is identical, so this delegates straight to requestAnimationFrame.
|
|
6
|
+
//
|
|
7
|
+
// SLOT: `useAnimationFrame` is a plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
8
|
+
import { useLayoutEffect, useMemo, useRef } from 'octane';
|
|
9
|
+
|
|
10
|
+
import { S, splitSlot, subSlot } from '../internal';
|
|
11
|
+
|
|
12
|
+
export const AnimationFrame = {
|
|
13
|
+
request(fn: FrameRequestCallback): number {
|
|
14
|
+
return requestAnimationFrame(fn);
|
|
15
|
+
},
|
|
16
|
+
cancel(id: number | null): void {
|
|
17
|
+
if (id != null) {
|
|
18
|
+
cancelAnimationFrame(id);
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface Frame {
|
|
24
|
+
request: (fn: FrameRequestCallback) => number;
|
|
25
|
+
cancel: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useAnimationFrame(...args: any[]): Frame {
|
|
29
|
+
const [, slotArg] = splitSlot(['_', ...args]);
|
|
30
|
+
const slot = slotArg ?? S('useAnimationFrame');
|
|
31
|
+
const idRef = useRef<number | null>(null, subSlot(slot, 'id'));
|
|
32
|
+
|
|
33
|
+
const frame = useMemo<Frame>(
|
|
34
|
+
() => ({
|
|
35
|
+
request(fn: FrameRequestCallback): number {
|
|
36
|
+
if (idRef.current != null) {
|
|
37
|
+
cancelAnimationFrame(idRef.current);
|
|
38
|
+
}
|
|
39
|
+
idRef.current = requestAnimationFrame((ts) => {
|
|
40
|
+
idRef.current = null;
|
|
41
|
+
fn(ts);
|
|
42
|
+
});
|
|
43
|
+
return idRef.current;
|
|
44
|
+
},
|
|
45
|
+
cancel(): void {
|
|
46
|
+
if (idRef.current != null) {
|
|
47
|
+
cancelAnimationFrame(idRef.current);
|
|
48
|
+
idRef.current = null;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
[],
|
|
53
|
+
subSlot(slot, 'frame'),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
useLayoutEffect(() => () => frame.cancel(), [], subSlot(slot, 'e:clean'));
|
|
57
|
+
|
|
58
|
+
return frame;
|
|
59
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/internals/useAnimationsFinished.ts. Returns a
|
|
2
|
+
// stable function that runs a callback once all CSS animations/transitions on the element
|
|
3
|
+
// have finished (or immediately if there are none / no element). octane: `ReactDOM.flushSync`
|
|
4
|
+
// → octane `flushSync`.
|
|
5
|
+
//
|
|
6
|
+
// SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
7
|
+
import { flushSync } from 'octane';
|
|
8
|
+
|
|
9
|
+
import { S, splitSlot, subSlot } from '../internal';
|
|
10
|
+
import { useAnimationFrame } from './useAnimationFrame';
|
|
11
|
+
import { useStableCallback } from './useStableCallback';
|
|
12
|
+
import { resolveRef } from './resolveRef';
|
|
13
|
+
|
|
14
|
+
const STARTING_STYLE_ATTRIBUTE = 'data-starting-style';
|
|
15
|
+
|
|
16
|
+
export function useAnimationsFinished(
|
|
17
|
+
...args: any[]
|
|
18
|
+
): (fnToExecute: () => void, signal?: AbortSignal | null) => void {
|
|
19
|
+
const [user, slotArg] = splitSlot(args);
|
|
20
|
+
const slot = slotArg ?? S('useAnimationsFinished');
|
|
21
|
+
const elementOrRef = user[0] as { current: HTMLElement | null } | HTMLElement | null;
|
|
22
|
+
const waitForStartingStyleRemoved = (user[1] as boolean | undefined) ?? false;
|
|
23
|
+
const treatAbortedAsFinished = (user[2] as boolean | undefined) ?? true;
|
|
24
|
+
|
|
25
|
+
const frame = useAnimationFrame(subSlot(slot, 'frame'));
|
|
26
|
+
|
|
27
|
+
return useStableCallback(
|
|
28
|
+
(fnToExecute: () => void, signal: AbortSignal | null = null) => {
|
|
29
|
+
frame.cancel();
|
|
30
|
+
|
|
31
|
+
const element = resolveRef(elementOrRef as any) as HTMLElement | null;
|
|
32
|
+
if (element == null) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const resolvedElement = element;
|
|
36
|
+
|
|
37
|
+
const done = () => {
|
|
38
|
+
// Synchronously flush so the browser doesn't paint an intermediate frame.
|
|
39
|
+
flushSync(fnToExecute);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
typeof resolvedElement.getAnimations !== 'function' ||
|
|
44
|
+
(globalThis as any).BASE_UI_ANIMATIONS_DISABLED
|
|
45
|
+
) {
|
|
46
|
+
fnToExecute();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function exec() {
|
|
51
|
+
Promise.all(resolvedElement.getAnimations().map((animation) => animation.finished))
|
|
52
|
+
.then(() => {
|
|
53
|
+
if (!signal?.aborted) {
|
|
54
|
+
done();
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
.catch(() => {
|
|
58
|
+
if (treatAbortedAsFinished) {
|
|
59
|
+
if (!signal?.aborted) {
|
|
60
|
+
done();
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const currentAnimations = resolvedElement.getAnimations();
|
|
65
|
+
if (
|
|
66
|
+
!signal?.aborted &&
|
|
67
|
+
currentAnimations.length > 0 &&
|
|
68
|
+
currentAnimations.some(
|
|
69
|
+
(animation) => animation.pending || animation.playState !== 'finished',
|
|
70
|
+
)
|
|
71
|
+
) {
|
|
72
|
+
exec();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (waitForStartingStyleRemoved) {
|
|
78
|
+
if (!resolvedElement.hasAttribute(STARTING_STYLE_ATTRIBUTE)) {
|
|
79
|
+
frame.request(exec);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const attributeObserver = new MutationObserver(() => {
|
|
83
|
+
if (!resolvedElement.hasAttribute(STARTING_STYLE_ATTRIBUTE)) {
|
|
84
|
+
attributeObserver.disconnect();
|
|
85
|
+
exec();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
attributeObserver.observe(resolvedElement, {
|
|
89
|
+
attributes: true,
|
|
90
|
+
attributeFilter: [STARTING_STYLE_ATTRIBUTE],
|
|
91
|
+
});
|
|
92
|
+
signal?.addEventListener('abort', () => attributeObserver.disconnect(), { once: true });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
frame.request(exec);
|
|
97
|
+
},
|
|
98
|
+
subSlot(slot, 'cb'),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/internals/useBaseUiId.ts (v1.6.0), which
|
|
2
|
+
// wraps @base-ui/utils/useId with the 'base-ui' prefix. Base UI's util calls
|
|
3
|
+
// React.useId() unconditionally, then returns `idOverride ?? `base-ui-${reactId}``.
|
|
4
|
+
// octane's useId is always available and SSR-stable, so React's mount-guard/global
|
|
5
|
+
// fallback dance collapses away — a caller-supplied id wins; otherwise the generated
|
|
6
|
+
// id is `base-ui-<octaneId>`.
|
|
7
|
+
//
|
|
8
|
+
// SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
9
|
+
import { useId as octaneUseId } from 'octane';
|
|
10
|
+
|
|
11
|
+
import { S, splitSlot } from '../internal';
|
|
12
|
+
|
|
13
|
+
export function useBaseUiId(...args: any[]): string | undefined {
|
|
14
|
+
const [user, slotArg] = splitSlot(args);
|
|
15
|
+
const slot = slotArg ?? S('useBaseUiId');
|
|
16
|
+
const idOverride = user[0] as string | undefined;
|
|
17
|
+
const id = octaneUseId(slot);
|
|
18
|
+
return idOverride ?? `base-ui-${id}`;
|
|
19
|
+
}
|