@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,217 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/internals/use-button/useButton.ts (v1.6.0).
|
|
2
|
+
// Produces `getButtonProps` (a prop-merger that adds `type="button"`/`role="button"`,
|
|
3
|
+
// the disabled/aria/tabIndex props, and keyboard-accessibility handlers) plus a
|
|
4
|
+
// `buttonRef`. octane adaptations: events are NATIVE (not synthetic), so the handlers act
|
|
5
|
+
// on the native event directly and `makeEventPreventable` shims `preventBaseUIHandler` /
|
|
6
|
+
// `baseUIHandlerPrevented` onto it; the dev native-<button> warning is dropped.
|
|
7
|
+
//
|
|
8
|
+
// SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
9
|
+
import { useCallback, useLayoutEffect, useRef } from 'octane';
|
|
10
|
+
|
|
11
|
+
import { S, splitSlot, subSlot } from '../internal';
|
|
12
|
+
import { mergeProps, makeEventPreventable } from './mergeProps';
|
|
13
|
+
import { useCompositeRootContext } from './CompositeRootContext';
|
|
14
|
+
import { useFocusableWhenDisabled } from './useFocusableWhenDisabled';
|
|
15
|
+
import { useStableCallback } from './useStableCallback';
|
|
16
|
+
|
|
17
|
+
export interface UseButtonParameters {
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
focusableWhenDisabled?: boolean;
|
|
20
|
+
tabIndex?: number;
|
|
21
|
+
native?: boolean;
|
|
22
|
+
composite?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UseButtonReturnValue {
|
|
26
|
+
getButtonProps: (externalProps?: Record<string, any>) => Record<string, any>;
|
|
27
|
+
buttonRef: (element: HTMLElement | null) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isButtonElement(elem: Element | null): elem is HTMLButtonElement {
|
|
31
|
+
return elem != null && elem instanceof HTMLElement && elem.tagName === 'BUTTON';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isValidLinkElement(elem: Element | null): elem is HTMLAnchorElement {
|
|
35
|
+
return Boolean(elem != null && elem.tagName === 'A' && (elem as HTMLAnchorElement).href);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useButton(...args: any[]): UseButtonReturnValue {
|
|
39
|
+
const [user, slotArg] = splitSlot(args);
|
|
40
|
+
const slot = slotArg ?? S('useButton');
|
|
41
|
+
const {
|
|
42
|
+
disabled = false,
|
|
43
|
+
focusableWhenDisabled,
|
|
44
|
+
tabIndex = 0,
|
|
45
|
+
native: isNativeButton = true,
|
|
46
|
+
composite: compositeProp,
|
|
47
|
+
} = (user[0] as UseButtonParameters) ?? {};
|
|
48
|
+
|
|
49
|
+
const elementRef = useRef<HTMLElement | null>(null, subSlot(slot, 'el'));
|
|
50
|
+
|
|
51
|
+
const compositeRootContext = useCompositeRootContext(true);
|
|
52
|
+
const isCompositeItem = compositeProp ?? compositeRootContext !== undefined;
|
|
53
|
+
|
|
54
|
+
const { props: focusableWhenDisabledProps } = useFocusableWhenDisabled(
|
|
55
|
+
{ focusableWhenDisabled, disabled, composite: isCompositeItem, tabIndex, isNativeButton },
|
|
56
|
+
subSlot(slot, 'fwd'),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Handle a disabled composite button that renders another button — the `disabled` prop
|
|
60
|
+
// passes through two `useButton`s, then the attribute is removed from the DOM.
|
|
61
|
+
const updateDisabled = useCallback(
|
|
62
|
+
() => {
|
|
63
|
+
const element = elementRef.current;
|
|
64
|
+
if (!isButtonElement(element)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
isCompositeItem &&
|
|
69
|
+
disabled &&
|
|
70
|
+
(focusableWhenDisabledProps as any).disabled === undefined &&
|
|
71
|
+
element.disabled
|
|
72
|
+
) {
|
|
73
|
+
element.disabled = false;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
[disabled, (focusableWhenDisabledProps as any).disabled, isCompositeItem],
|
|
77
|
+
subSlot(slot, 'upd'),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
useLayoutEffect(updateDisabled, [updateDisabled], subSlot(slot, 'e:upd'));
|
|
81
|
+
|
|
82
|
+
const getButtonProps = useCallback(
|
|
83
|
+
(externalProps: Record<string, any> = {}) => {
|
|
84
|
+
const {
|
|
85
|
+
onClick: externalOnClick,
|
|
86
|
+
onMouseDown: externalOnMouseDown,
|
|
87
|
+
onKeyUp: externalOnKeyUp,
|
|
88
|
+
onKeyDown: externalOnKeyDown,
|
|
89
|
+
onPointerDown: externalOnPointerDown,
|
|
90
|
+
...otherExternalProps
|
|
91
|
+
} = externalProps;
|
|
92
|
+
|
|
93
|
+
return mergeProps(
|
|
94
|
+
{
|
|
95
|
+
onClick(event: any) {
|
|
96
|
+
if (disabled) {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
externalOnClick?.(event);
|
|
101
|
+
},
|
|
102
|
+
onMouseDown(event: any) {
|
|
103
|
+
if (!disabled) {
|
|
104
|
+
externalOnMouseDown?.(event);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
onKeyDown(event: any) {
|
|
108
|
+
if (disabled) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
makeEventPreventable(event);
|
|
113
|
+
externalOnKeyDown?.(event);
|
|
114
|
+
if (event.baseUIHandlerPrevented) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const isCurrentTarget = event.target === event.currentTarget;
|
|
119
|
+
const currentTarget = event.currentTarget as HTMLElement;
|
|
120
|
+
const isButton = isButtonElement(currentTarget);
|
|
121
|
+
const isLink = !isNativeButton && isValidLinkElement(currentTarget);
|
|
122
|
+
const shouldClick = isCurrentTarget && (isNativeButton ? isButton : !isLink);
|
|
123
|
+
const isEnterKey = event.key === 'Enter';
|
|
124
|
+
const isSpaceKey = event.key === ' ';
|
|
125
|
+
const role = currentTarget.getAttribute('role');
|
|
126
|
+
const isTextNavigationRole =
|
|
127
|
+
role?.startsWith('menuitem') || role === 'option' || role === 'gridcell';
|
|
128
|
+
|
|
129
|
+
if (isCurrentTarget && isCompositeItem && isSpaceKey) {
|
|
130
|
+
if (event.defaultPrevented && isTextNavigationRole) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
event.preventDefault();
|
|
135
|
+
|
|
136
|
+
if (isLink || (isNativeButton && isButton)) {
|
|
137
|
+
currentTarget.click();
|
|
138
|
+
event.preventBaseUIHandler();
|
|
139
|
+
} else if (shouldClick) {
|
|
140
|
+
externalOnClick?.(event);
|
|
141
|
+
event.preventBaseUIHandler();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Keyboard accessibility for native and non-native elements.
|
|
148
|
+
if (shouldClick) {
|
|
149
|
+
if (!isNativeButton && (isSpaceKey || isEnterKey)) {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
}
|
|
152
|
+
if (!isNativeButton && isEnterKey) {
|
|
153
|
+
externalOnClick?.(event);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
onKeyUp(event: any) {
|
|
158
|
+
if (disabled) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// preventDefault in keyUp on a <button> won't dispatch a click if Space is pressed.
|
|
163
|
+
makeEventPreventable(event);
|
|
164
|
+
externalOnKeyUp?.(event);
|
|
165
|
+
|
|
166
|
+
if (
|
|
167
|
+
event.target === event.currentTarget &&
|
|
168
|
+
isNativeButton &&
|
|
169
|
+
isCompositeItem &&
|
|
170
|
+
isButtonElement(event.currentTarget as HTMLElement) &&
|
|
171
|
+
event.key === ' '
|
|
172
|
+
) {
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (event.baseUIHandlerPrevented) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Keyboard accessibility for non-interactive elements.
|
|
182
|
+
if (
|
|
183
|
+
event.target === event.currentTarget &&
|
|
184
|
+
!isNativeButton &&
|
|
185
|
+
!isCompositeItem &&
|
|
186
|
+
event.key === ' '
|
|
187
|
+
) {
|
|
188
|
+
externalOnClick?.(event);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
onPointerDown(event: any) {
|
|
192
|
+
if (disabled) {
|
|
193
|
+
event.preventDefault();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
externalOnPointerDown?.(event);
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
isNativeButton ? { type: 'button' } : { role: 'button' },
|
|
200
|
+
focusableWhenDisabledProps,
|
|
201
|
+
otherExternalProps,
|
|
202
|
+
);
|
|
203
|
+
},
|
|
204
|
+
[disabled, focusableWhenDisabledProps, isCompositeItem, isNativeButton],
|
|
205
|
+
subSlot(slot, 'gbp'),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const buttonRef = useStableCallback(
|
|
209
|
+
(element: HTMLElement | null) => {
|
|
210
|
+
elementRef.current = element;
|
|
211
|
+
updateDisabled();
|
|
212
|
+
},
|
|
213
|
+
subSlot(slot, 'ref'),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
return { getButtonProps, buttonRef };
|
|
217
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/checkbox-group/useCheckboxGroupParent.ts. Drives a
|
|
2
|
+
// "select-all" parent checkbox for a CheckboxGroup: checked when all children are on,
|
|
3
|
+
// indeterminate when some are, and a 3-state toggle cycle honoring disabled children.
|
|
4
|
+
//
|
|
5
|
+
// SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
6
|
+
import { useCallback, useMemo, useRef, useState } from 'octane';
|
|
7
|
+
|
|
8
|
+
import { S, splitSlot, subSlot } from '../internal';
|
|
9
|
+
import { useBaseUiId } from './useBaseUiId';
|
|
10
|
+
import { useStableCallback } from './useStableCallback';
|
|
11
|
+
|
|
12
|
+
const EMPTY: string[] = [];
|
|
13
|
+
|
|
14
|
+
export interface UseCheckboxGroupParentParameters {
|
|
15
|
+
allValues?: string[];
|
|
16
|
+
value?: string[];
|
|
17
|
+
onValueChange?: (value: string[], eventDetails: any) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useCheckboxGroupParent(...args: any[]): any {
|
|
21
|
+
const [user, slotArg] = splitSlot(args);
|
|
22
|
+
const slot = slotArg ?? S('useCheckboxGroupParent');
|
|
23
|
+
const {
|
|
24
|
+
allValues = EMPTY,
|
|
25
|
+
value = EMPTY,
|
|
26
|
+
onValueChange: onValueChangeProp,
|
|
27
|
+
} = (user[0] as UseCheckboxGroupParentParameters) ?? {};
|
|
28
|
+
|
|
29
|
+
const uncontrolledStateRef = useRef(value, subSlot(slot, 'uncontrolled'));
|
|
30
|
+
const disabledStatesRef = useRef(new Map<string, boolean>(), subSlot(slot, 'disabledStates'));
|
|
31
|
+
|
|
32
|
+
const [status, setStatus] = useState<'on' | 'off' | 'mixed'>('mixed', subSlot(slot, 'status'));
|
|
33
|
+
|
|
34
|
+
const id = useBaseUiId(undefined, subSlot(slot, 'id'));
|
|
35
|
+
const checked = value.length === allValues.length;
|
|
36
|
+
const indeterminate = value.length !== allValues.length && value.length > 0;
|
|
37
|
+
|
|
38
|
+
const onValueChange = useStableCallback(onValueChangeProp, subSlot(slot, 'onValueChange'));
|
|
39
|
+
|
|
40
|
+
const getParentProps = useCallback(
|
|
41
|
+
() => ({
|
|
42
|
+
id,
|
|
43
|
+
indeterminate,
|
|
44
|
+
checked,
|
|
45
|
+
'aria-controls': allValues.map((v) => `${id}-${v}`).join(' '),
|
|
46
|
+
onCheckedChange(_: boolean, eventDetails: any) {
|
|
47
|
+
const uncontrolledState = uncontrolledStateRef.current;
|
|
48
|
+
const none = allValues.filter(
|
|
49
|
+
(v) => disabledStatesRef.current.get(v) && uncontrolledState.includes(v),
|
|
50
|
+
);
|
|
51
|
+
const all = allValues.filter(
|
|
52
|
+
(v) =>
|
|
53
|
+
!disabledStatesRef.current.get(v) ||
|
|
54
|
+
(disabledStatesRef.current.get(v) && uncontrolledState.includes(v)),
|
|
55
|
+
);
|
|
56
|
+
const allOnOrOff =
|
|
57
|
+
uncontrolledState.length === all.length || uncontrolledState.length === 0;
|
|
58
|
+
|
|
59
|
+
if (allOnOrOff) {
|
|
60
|
+
if (value.length === all.length) {
|
|
61
|
+
onValueChange(none, eventDetails);
|
|
62
|
+
} else {
|
|
63
|
+
onValueChange(all, eventDetails);
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let nextStatus: 'on' | 'off' | 'mixed' = 'mixed';
|
|
69
|
+
let nextValue = uncontrolledState;
|
|
70
|
+
if (status === 'mixed') {
|
|
71
|
+
nextStatus = 'on';
|
|
72
|
+
nextValue = all;
|
|
73
|
+
} else if (status === 'on') {
|
|
74
|
+
nextStatus = 'off';
|
|
75
|
+
nextValue = none;
|
|
76
|
+
}
|
|
77
|
+
onValueChange(nextValue, eventDetails);
|
|
78
|
+
if (!eventDetails.isCanceled) {
|
|
79
|
+
setStatus(nextStatus);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
[allValues, checked, id, indeterminate, onValueChange, status, value.length],
|
|
84
|
+
subSlot(slot, 'getParentProps'),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const getChildProps = useCallback(
|
|
88
|
+
(childValue: string) => ({
|
|
89
|
+
checked: value.includes(childValue),
|
|
90
|
+
onCheckedChange(nextChecked: boolean, eventDetails: any) {
|
|
91
|
+
const newValue = value.slice();
|
|
92
|
+
if (nextChecked) {
|
|
93
|
+
newValue.push(childValue);
|
|
94
|
+
} else {
|
|
95
|
+
newValue.splice(newValue.indexOf(childValue), 1);
|
|
96
|
+
}
|
|
97
|
+
onValueChange(newValue, eventDetails);
|
|
98
|
+
if (!eventDetails.isCanceled) {
|
|
99
|
+
uncontrolledStateRef.current = newValue;
|
|
100
|
+
setStatus('mixed');
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
[onValueChange, value],
|
|
105
|
+
subSlot(slot, 'getChildProps'),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return useMemo(
|
|
109
|
+
() => ({ id, indeterminate, getParentProps, getChildProps, disabledStatesRef }),
|
|
110
|
+
[id, indeterminate, getParentProps, getChildProps],
|
|
111
|
+
subSlot(slot, 'return'),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/utils/src/useControlled.ts. The controlled/uncontrolled
|
|
2
|
+
// state hook: `controlled !== undefined` decides (once, at first render) whether the
|
|
3
|
+
// component is controlled; the setter only writes local state when uncontrolled. Base UI's
|
|
4
|
+
// dev warnings (switching controlled↔uncontrolled, changing the default) are dropped
|
|
5
|
+
// (dev-only surface).
|
|
6
|
+
//
|
|
7
|
+
// SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
8
|
+
import { useCallback, useRef, useState } from 'octane';
|
|
9
|
+
|
|
10
|
+
import { S, splitSlot, subSlot } from '../internal';
|
|
11
|
+
|
|
12
|
+
export interface UseControlledProps<T = unknown> {
|
|
13
|
+
controlled: T | undefined;
|
|
14
|
+
default: T | undefined;
|
|
15
|
+
name: string;
|
|
16
|
+
state?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useControlled<T = unknown>(
|
|
20
|
+
...args: any[]
|
|
21
|
+
): [T, (newValue: T | ((prevValue: T) => T)) => void] {
|
|
22
|
+
const [user, slotArg] = splitSlot(args);
|
|
23
|
+
const slot = slotArg ?? S('useControlled');
|
|
24
|
+
const { controlled, default: defaultProp } = user[0] as UseControlledProps<T>;
|
|
25
|
+
|
|
26
|
+
// `isControlled` never changes over the component's lifetime.
|
|
27
|
+
const { current: isControlled } = useRef(controlled !== undefined, subSlot(slot, 'ctrl'));
|
|
28
|
+
const [valueState, setValue] = useState(defaultProp, subSlot(slot, 'val'));
|
|
29
|
+
const value = isControlled ? controlled : valueState;
|
|
30
|
+
|
|
31
|
+
const setValueIfUncontrolled = useCallback(
|
|
32
|
+
(newValue: T | ((prevValue: T) => T)) => {
|
|
33
|
+
if (!isControlled) {
|
|
34
|
+
setValue(newValue as any);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
[],
|
|
38
|
+
subSlot(slot, 'set'),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return [value as T, setValueIfUncontrolled];
|
|
42
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/utils/useFocusableWhenDisabled.ts.
|
|
2
|
+
// Computes the tabIndex / aria-disabled / disabled / onKeyDown props a button-like element
|
|
3
|
+
// needs so it can (optionally) stay focusable while disabled. Pure prop computation.
|
|
4
|
+
//
|
|
5
|
+
// SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
6
|
+
import { useMemo } from 'octane';
|
|
7
|
+
|
|
8
|
+
import { S, splitSlot } from '../internal';
|
|
9
|
+
|
|
10
|
+
export interface UseFocusableWhenDisabledParameters {
|
|
11
|
+
focusableWhenDisabled?: boolean;
|
|
12
|
+
disabled: boolean;
|
|
13
|
+
composite?: boolean;
|
|
14
|
+
tabIndex?: number;
|
|
15
|
+
isNativeButton: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FocusableWhenDisabledProps {
|
|
19
|
+
'aria-disabled'?: boolean;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
onKeyDown: (event: KeyboardEvent) => void;
|
|
22
|
+
tabIndex: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useFocusableWhenDisabled(...args: any[]): { props: FocusableWhenDisabledProps } {
|
|
26
|
+
const [user, slotArg] = splitSlot(args);
|
|
27
|
+
const slot = slotArg ?? S('useFocusableWhenDisabled');
|
|
28
|
+
const {
|
|
29
|
+
focusableWhenDisabled,
|
|
30
|
+
disabled,
|
|
31
|
+
composite = false,
|
|
32
|
+
tabIndex: tabIndexProp = 0,
|
|
33
|
+
isNativeButton,
|
|
34
|
+
} = user[0] as UseFocusableWhenDisabledParameters;
|
|
35
|
+
|
|
36
|
+
const isFocusableComposite = composite && focusableWhenDisabled !== false;
|
|
37
|
+
const isNonFocusableComposite = composite && focusableWhenDisabled === false;
|
|
38
|
+
|
|
39
|
+
// We can't explicitly assign `undefined` to any of these props — that would prevent a
|
|
40
|
+
// subsequently merged prop from setting them.
|
|
41
|
+
const props = useMemo(
|
|
42
|
+
() => {
|
|
43
|
+
const additionalProps = {
|
|
44
|
+
onKeyDown(event: KeyboardEvent) {
|
|
45
|
+
if (disabled && focusableWhenDisabled && event.key !== 'Tab') {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
} as FocusableWhenDisabledProps;
|
|
50
|
+
|
|
51
|
+
if (!composite) {
|
|
52
|
+
additionalProps.tabIndex = tabIndexProp;
|
|
53
|
+
if (!isNativeButton && disabled) {
|
|
54
|
+
additionalProps.tabIndex = focusableWhenDisabled ? tabIndexProp : -1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
(isNativeButton && (focusableWhenDisabled || isFocusableComposite)) ||
|
|
60
|
+
(!isNativeButton && disabled)
|
|
61
|
+
) {
|
|
62
|
+
additionalProps['aria-disabled'] = disabled;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (isNativeButton && (!focusableWhenDisabled || isNonFocusableComposite)) {
|
|
66
|
+
additionalProps.disabled = disabled;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return additionalProps;
|
|
70
|
+
},
|
|
71
|
+
[
|
|
72
|
+
composite,
|
|
73
|
+
disabled,
|
|
74
|
+
focusableWhenDisabled,
|
|
75
|
+
isFocusableComposite,
|
|
76
|
+
isNonFocusableComposite,
|
|
77
|
+
isNativeButton,
|
|
78
|
+
tabIndexProp,
|
|
79
|
+
],
|
|
80
|
+
slot,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return { props };
|
|
84
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/utils/src/useForcedRerendering.ts.
|
|
2
|
+
// SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
3
|
+
import { useCallback, useState } from 'octane';
|
|
4
|
+
import { S, splitSlot, subSlot } from '../internal';
|
|
5
|
+
|
|
6
|
+
export function useForcedRerendering(...args: any[]): () => void {
|
|
7
|
+
const [, slotArg] = splitSlot(['_', ...args]);
|
|
8
|
+
const slot = slotArg ?? S('useForcedRerendering');
|
|
9
|
+
const [, setState] = useState({}, subSlot(slot, 'state'));
|
|
10
|
+
return useCallback(() => setState({}), [], subSlot(slot, 'cb'));
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/utils/useIsHydrating.ts. React drives this via
|
|
2
|
+
// `useSyncExternalStore` (server snapshot `true`, client snapshot `false`) so the first client
|
|
3
|
+
// paint of a hydrated tree matches the server, then flips to `false`. octane's binding renders
|
|
4
|
+
// client-only in these tests, so a fresh mount is never hydrating — returns `false`. It only
|
|
5
|
+
// gates the `thumbAlignment: 'edge'` pre-hydration visibility path (inert for the default
|
|
6
|
+
// `center` alignment).
|
|
7
|
+
export function useIsHydrating(): boolean {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/internals/useOpenChangeComplete.tsx. Calls
|
|
2
|
+
// `onComplete` once the element's open/close CSS animation finishes (or immediately when
|
|
3
|
+
// there's no animation / no element).
|
|
4
|
+
//
|
|
5
|
+
// SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
6
|
+
import { useEffect } from 'octane';
|
|
7
|
+
|
|
8
|
+
import { S, splitSlot, subSlot } from '../internal';
|
|
9
|
+
import { useStableCallback } from './useStableCallback';
|
|
10
|
+
import { useAnimationsFinished } from './useAnimationsFinished';
|
|
11
|
+
|
|
12
|
+
export interface UseOpenChangeCompleteParameters {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
open?: boolean;
|
|
15
|
+
ref: { current: HTMLElement | null };
|
|
16
|
+
onComplete: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useOpenChangeComplete(...args: any[]): void {
|
|
20
|
+
const [user, slotArg] = splitSlot(args);
|
|
21
|
+
const slot = slotArg ?? S('useOpenChangeComplete');
|
|
22
|
+
const {
|
|
23
|
+
enabled = true,
|
|
24
|
+
open,
|
|
25
|
+
ref,
|
|
26
|
+
onComplete: onCompleteParam,
|
|
27
|
+
} = user[0] as UseOpenChangeCompleteParameters;
|
|
28
|
+
|
|
29
|
+
const onComplete = useStableCallback(onCompleteParam, subSlot(slot, 'oc'));
|
|
30
|
+
const runOnceAnimationsFinish = useAnimationsFinished(ref, open, false, subSlot(slot, 'raf'));
|
|
31
|
+
|
|
32
|
+
useEffect(
|
|
33
|
+
() => {
|
|
34
|
+
if (!enabled) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const abortController = new AbortController();
|
|
38
|
+
runOnceAnimationsFinish(onComplete, abortController.signal);
|
|
39
|
+
return () => {
|
|
40
|
+
abortController.abort();
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
[enabled, open, onComplete, runOnceAnimationsFinish],
|
|
44
|
+
subSlot(slot, 'e:run'),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Minimal stand-in for .base-ui/packages/react/src/internals/usePressAndHold.ts. The full hook
|
|
2
|
+
// implements hold-to-auto-repeat + touch/scroll heuristics (288 lines, deferred). This stub
|
|
3
|
+
// provides inert pointer handlers so single-press stepping (via the button's own onClick) works
|
|
4
|
+
// and never skips a click. TODO: port the full auto-repeat behavior + dedicated timing tests.
|
|
5
|
+
export function isTouchLikePointerType(pointerType: string): boolean {
|
|
6
|
+
return pointerType === 'touch' || pointerType === 'pen';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UsePressAndHoldParameters {
|
|
10
|
+
disabled: boolean;
|
|
11
|
+
readOnly?: boolean;
|
|
12
|
+
tick: (triggerEvent?: any) => boolean;
|
|
13
|
+
onStop?: (nativeEvent: any) => void;
|
|
14
|
+
tickDelay?: number;
|
|
15
|
+
startDelay?: number;
|
|
16
|
+
scrollDistance?: number;
|
|
17
|
+
elementRef: { current: HTMLElement | null };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function usePressAndHold(_params: UsePressAndHoldParameters): {
|
|
21
|
+
pointerHandlers: Record<string, (event: any) => void>;
|
|
22
|
+
shouldSkipClick: (event: any) => boolean;
|
|
23
|
+
} {
|
|
24
|
+
return {
|
|
25
|
+
pointerHandlers: {
|
|
26
|
+
onTouchStart() {},
|
|
27
|
+
onTouchEnd() {},
|
|
28
|
+
onPointerDown() {},
|
|
29
|
+
onPointerUp() {},
|
|
30
|
+
onPointerMove() {},
|
|
31
|
+
onPointerLeave() {},
|
|
32
|
+
},
|
|
33
|
+
shouldSkipClick: () => false,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/utils/src/useRefWithInit.ts. A ref whose `.current` is
|
|
2
|
+
// lazily initialized once via `init(arg)` — avoids re-running the factory (and the
|
|
3
|
+
// allocation) on every render that a plain `useRef(init())` would incur.
|
|
4
|
+
//
|
|
5
|
+
// SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
6
|
+
import { useRef } from 'octane';
|
|
7
|
+
|
|
8
|
+
import { S, splitSlot, subSlot } from '../internal';
|
|
9
|
+
|
|
10
|
+
const UNSET: unique symbol = Symbol('base-ui.useRefWithInit.unset');
|
|
11
|
+
|
|
12
|
+
export function useRefWithInit<T, A = undefined>(...args: any[]): { current: T } {
|
|
13
|
+
const [user, slotArg] = splitSlot(args);
|
|
14
|
+
const slot = slotArg ?? S('useRefWithInit');
|
|
15
|
+
const init = user[0] as (arg?: A) => T;
|
|
16
|
+
const arg = user[1] as A | undefined;
|
|
17
|
+
|
|
18
|
+
const ref = useRef<T | typeof UNSET>(UNSET, subSlot(slot, 'ref'));
|
|
19
|
+
if (ref.current === UNSET) {
|
|
20
|
+
ref.current = init(arg);
|
|
21
|
+
}
|
|
22
|
+
return ref as { current: T };
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/utils/useRegisteredLabelId.ts. Generates a
|
|
2
|
+
// stable id (via useBaseUiId) and registers it with the owning Root through a layout
|
|
3
|
+
// effect (`setLabelId(id)`; cleared on unmount) — the Root reflects it as
|
|
4
|
+
// `aria-labelledby`. Shared by Meter.Label / Progress.Label.
|
|
5
|
+
//
|
|
6
|
+
// SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
7
|
+
import { useLayoutEffect } from 'octane';
|
|
8
|
+
|
|
9
|
+
import { S, splitSlot, subSlot } from '../internal';
|
|
10
|
+
import { useBaseUiId } from './useBaseUiId';
|
|
11
|
+
|
|
12
|
+
export function useRegisteredLabelId(...args: any[]): string | undefined {
|
|
13
|
+
const [user, slotArg] = splitSlot(args);
|
|
14
|
+
const slot = slotArg ?? S('useRegisteredLabelId');
|
|
15
|
+
const idProp = user[0] as string | undefined;
|
|
16
|
+
const setLabelId = user[1] as (id: string | undefined) => void;
|
|
17
|
+
|
|
18
|
+
const id = useBaseUiId(idProp, subSlot(slot, 'id'));
|
|
19
|
+
|
|
20
|
+
useLayoutEffect(
|
|
21
|
+
() => {
|
|
22
|
+
setLabelId(id);
|
|
23
|
+
return () => {
|
|
24
|
+
setLabelId(undefined);
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
[id, setLabelId],
|
|
28
|
+
subSlot(slot, 'e:label'),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return id;
|
|
32
|
+
}
|