@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,179 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/internals/composite/composite.ts (the constants,
|
|
2
|
+
// `isNativeInput`, `scrollIntoViewIfNeeded`) + `@base-ui/utils/isElementDisabled`. Pure DOM
|
|
3
|
+
// helpers — framework-agnostic, ported verbatim.
|
|
4
|
+
|
|
5
|
+
// .base-ui/…/composite/constants.ts — marks the item that should hold the initial tab stop.
|
|
6
|
+
export const ACTIVE_COMPOSITE_ITEM = 'data-composite-item-active';
|
|
7
|
+
|
|
8
|
+
export const ARROW_UP = 'ArrowUp';
|
|
9
|
+
export const ARROW_DOWN = 'ArrowDown';
|
|
10
|
+
export const ARROW_LEFT = 'ArrowLeft';
|
|
11
|
+
export const ARROW_RIGHT = 'ArrowRight';
|
|
12
|
+
export const HOME = 'Home';
|
|
13
|
+
export const END = 'End';
|
|
14
|
+
export const PAGE_UP = 'PageUp';
|
|
15
|
+
export const PAGE_DOWN = 'PageDown';
|
|
16
|
+
|
|
17
|
+
export const HORIZONTAL_KEYS = new Set([ARROW_LEFT, ARROW_RIGHT]);
|
|
18
|
+
export const HORIZONTAL_KEYS_WITH_EXTRA_KEYS = new Set([ARROW_LEFT, ARROW_RIGHT, HOME, END]);
|
|
19
|
+
export const VERTICAL_KEYS = new Set([ARROW_UP, ARROW_DOWN]);
|
|
20
|
+
export const VERTICAL_KEYS_WITH_EXTRA_KEYS = new Set([ARROW_UP, ARROW_DOWN, HOME, END]);
|
|
21
|
+
export const ARROW_KEYS = new Set([...HORIZONTAL_KEYS, ...VERTICAL_KEYS]);
|
|
22
|
+
export const COMPOSITE_KEYS = new Set([...ARROW_KEYS, HOME, END]);
|
|
23
|
+
|
|
24
|
+
export const SHIFT = 'Shift' as const;
|
|
25
|
+
export const CONTROL = 'Control' as const;
|
|
26
|
+
export const ALT = 'Alt' as const;
|
|
27
|
+
export const META = 'Meta' as const;
|
|
28
|
+
export const MODIFIER_KEYS = new Set([SHIFT, CONTROL, ALT, META] as const);
|
|
29
|
+
export type ModifierKey = 'Shift' | 'Control' | 'Alt' | 'Meta';
|
|
30
|
+
|
|
31
|
+
export type TextDirection = 'ltr' | 'rtl';
|
|
32
|
+
|
|
33
|
+
function isHTMLElement(el: unknown): el is HTMLElement {
|
|
34
|
+
return el != null && el instanceof HTMLElement;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isInputElement(element: EventTarget): element is HTMLInputElement {
|
|
38
|
+
return isHTMLElement(element) && element.tagName === 'INPUT';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isNativeInput(
|
|
42
|
+
element: EventTarget,
|
|
43
|
+
): element is HTMLElement & (HTMLInputElement | HTMLTextAreaElement) {
|
|
44
|
+
if (isInputElement(element) && element.selectionStart != null) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (isHTMLElement(element) && element.tagName === 'TEXTAREA') {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isElementDisabled(element: HTMLElement | null): boolean {
|
|
54
|
+
return (
|
|
55
|
+
element == null ||
|
|
56
|
+
element.hasAttribute('disabled') ||
|
|
57
|
+
element.getAttribute('aria-disabled') === 'true'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function scrollIntoViewIfNeeded(
|
|
62
|
+
scrollContainer: HTMLElement | null,
|
|
63
|
+
element: HTMLElement | null,
|
|
64
|
+
direction: TextDirection,
|
|
65
|
+
orientation: 'horizontal' | 'vertical' | 'both',
|
|
66
|
+
): void {
|
|
67
|
+
if (!scrollContainer || !element || !element.scrollTo) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let targetX = scrollContainer.scrollLeft;
|
|
72
|
+
let targetY = scrollContainer.scrollTop;
|
|
73
|
+
|
|
74
|
+
const isOverflowingX = scrollContainer.clientWidth < scrollContainer.scrollWidth;
|
|
75
|
+
const isOverflowingY = scrollContainer.clientHeight < scrollContainer.scrollHeight;
|
|
76
|
+
|
|
77
|
+
if (isOverflowingX && orientation !== 'vertical') {
|
|
78
|
+
const elementOffsetLeft = getOffset(scrollContainer, element, 'left');
|
|
79
|
+
const containerStyles = getStyles(scrollContainer);
|
|
80
|
+
const elementStyles = getStyles(element);
|
|
81
|
+
|
|
82
|
+
if (direction === 'ltr') {
|
|
83
|
+
if (
|
|
84
|
+
elementOffsetLeft + element.offsetWidth + elementStyles.scrollMarginRight >
|
|
85
|
+
scrollContainer.scrollLeft +
|
|
86
|
+
scrollContainer.clientWidth -
|
|
87
|
+
containerStyles.scrollPaddingRight
|
|
88
|
+
) {
|
|
89
|
+
targetX =
|
|
90
|
+
elementOffsetLeft +
|
|
91
|
+
element.offsetWidth +
|
|
92
|
+
elementStyles.scrollMarginRight -
|
|
93
|
+
scrollContainer.clientWidth +
|
|
94
|
+
containerStyles.scrollPaddingRight;
|
|
95
|
+
} else if (
|
|
96
|
+
elementOffsetLeft - elementStyles.scrollMarginLeft <
|
|
97
|
+
scrollContainer.scrollLeft + containerStyles.scrollPaddingLeft
|
|
98
|
+
) {
|
|
99
|
+
targetX =
|
|
100
|
+
elementOffsetLeft - elementStyles.scrollMarginLeft - containerStyles.scrollPaddingLeft;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (direction === 'rtl') {
|
|
105
|
+
if (
|
|
106
|
+
elementOffsetLeft - elementStyles.scrollMarginRight <
|
|
107
|
+
scrollContainer.scrollLeft + containerStyles.scrollPaddingLeft
|
|
108
|
+
) {
|
|
109
|
+
targetX =
|
|
110
|
+
elementOffsetLeft - elementStyles.scrollMarginLeft - containerStyles.scrollPaddingLeft;
|
|
111
|
+
} else if (
|
|
112
|
+
elementOffsetLeft + element.offsetWidth + elementStyles.scrollMarginRight >
|
|
113
|
+
scrollContainer.scrollLeft +
|
|
114
|
+
scrollContainer.clientWidth -
|
|
115
|
+
containerStyles.scrollPaddingRight
|
|
116
|
+
) {
|
|
117
|
+
targetX =
|
|
118
|
+
elementOffsetLeft +
|
|
119
|
+
element.offsetWidth +
|
|
120
|
+
elementStyles.scrollMarginRight -
|
|
121
|
+
scrollContainer.clientWidth +
|
|
122
|
+
containerStyles.scrollPaddingRight;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (isOverflowingY && orientation !== 'horizontal') {
|
|
128
|
+
const elementOffsetTop = getOffset(scrollContainer, element, 'top');
|
|
129
|
+
const containerStyles = getStyles(scrollContainer);
|
|
130
|
+
const elementStyles = getStyles(element);
|
|
131
|
+
|
|
132
|
+
if (
|
|
133
|
+
elementOffsetTop - elementStyles.scrollMarginTop <
|
|
134
|
+
scrollContainer.scrollTop + containerStyles.scrollPaddingTop
|
|
135
|
+
) {
|
|
136
|
+
targetY = elementOffsetTop - elementStyles.scrollMarginTop - containerStyles.scrollPaddingTop;
|
|
137
|
+
} else if (
|
|
138
|
+
elementOffsetTop + element.offsetHeight + elementStyles.scrollMarginBottom >
|
|
139
|
+
scrollContainer.scrollTop + scrollContainer.clientHeight - containerStyles.scrollPaddingBottom
|
|
140
|
+
) {
|
|
141
|
+
targetY =
|
|
142
|
+
elementOffsetTop +
|
|
143
|
+
element.offsetHeight +
|
|
144
|
+
elementStyles.scrollMarginBottom -
|
|
145
|
+
scrollContainer.clientHeight +
|
|
146
|
+
containerStyles.scrollPaddingBottom;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
scrollContainer.scrollTo({ left: targetX, top: targetY, behavior: 'auto' });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getOffset(ancestor: HTMLElement, element: HTMLElement, side: 'left' | 'top'): number {
|
|
154
|
+
const propName = side === 'left' ? 'offsetLeft' : 'offsetTop';
|
|
155
|
+
let result = 0;
|
|
156
|
+
let el = element;
|
|
157
|
+
while (el.offsetParent) {
|
|
158
|
+
result += el[propName];
|
|
159
|
+
if (el.offsetParent === ancestor) {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
el = el.offsetParent as HTMLElement;
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function getStyles(element: HTMLElement): Record<string, number> {
|
|
168
|
+
const styles = getComputedStyle(element);
|
|
169
|
+
return {
|
|
170
|
+
scrollMarginTop: parseFloat(styles.scrollMarginTop) || 0,
|
|
171
|
+
scrollMarginRight: parseFloat(styles.scrollMarginRight) || 0,
|
|
172
|
+
scrollMarginBottom: parseFloat(styles.scrollMarginBottom) || 0,
|
|
173
|
+
scrollMarginLeft: parseFloat(styles.scrollMarginLeft) || 0,
|
|
174
|
+
scrollPaddingTop: parseFloat(styles.scrollPaddingTop) || 0,
|
|
175
|
+
scrollPaddingRight: parseFloat(styles.scrollPaddingRight) || 0,
|
|
176
|
+
scrollPaddingBottom: parseFloat(styles.scrollPaddingBottom) || 0,
|
|
177
|
+
scrollPaddingLeft: parseFloat(styles.scrollPaddingLeft) || 0,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Vendored list-navigation helpers — Base UI itself vendors these from `floating-ui-react`
|
|
2
|
+
// (`.base-ui/packages/react/src/floating-ui-react/utils.ts`); octane's `@octanejs/floating-ui`
|
|
3
|
+
// implements the identical functions but does not export them publicly, so we keep a small
|
|
4
|
+
// self-contained copy here (matching the octane port's implementations verbatim).
|
|
5
|
+
|
|
6
|
+
export function getTarget(event: any): EventTarget | null {
|
|
7
|
+
if ('composedPath' in event) {
|
|
8
|
+
return event.composedPath()[0];
|
|
9
|
+
}
|
|
10
|
+
return event.target;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function stopEvent(event: any): void {
|
|
14
|
+
event.preventDefault();
|
|
15
|
+
event.stopPropagation();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isIndexOutOfListBounds(listRef: { current: any[] }, index: number): boolean {
|
|
19
|
+
return index < 0 || index >= listRef.current.length;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isListIndexDisabled(
|
|
23
|
+
listRef: { current: any[] },
|
|
24
|
+
index: number,
|
|
25
|
+
disabledIndices?: number[] | ((index: number) => boolean),
|
|
26
|
+
): boolean {
|
|
27
|
+
if (typeof disabledIndices === 'function') {
|
|
28
|
+
return disabledIndices(index);
|
|
29
|
+
}
|
|
30
|
+
if (disabledIndices) {
|
|
31
|
+
return disabledIndices.includes(index);
|
|
32
|
+
}
|
|
33
|
+
const element = listRef.current[index];
|
|
34
|
+
return (
|
|
35
|
+
element == null ||
|
|
36
|
+
element.hasAttribute('disabled') ||
|
|
37
|
+
element.getAttribute('aria-disabled') === 'true'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function findNonDisabledListIndex(
|
|
42
|
+
listRef: { current: any[] },
|
|
43
|
+
options: {
|
|
44
|
+
startingIndex?: number;
|
|
45
|
+
decrement?: boolean;
|
|
46
|
+
disabledIndices?: number[] | ((index: number) => boolean);
|
|
47
|
+
amount?: number;
|
|
48
|
+
} = {},
|
|
49
|
+
): number {
|
|
50
|
+
const { startingIndex = -1, decrement = false, disabledIndices, amount = 1 } = options;
|
|
51
|
+
let index = startingIndex;
|
|
52
|
+
do {
|
|
53
|
+
index += decrement ? -amount : amount;
|
|
54
|
+
} while (
|
|
55
|
+
index >= 0 &&
|
|
56
|
+
index <= listRef.current.length - 1 &&
|
|
57
|
+
isListIndexDisabled(listRef, index, disabledIndices)
|
|
58
|
+
);
|
|
59
|
+
return index;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getMinListIndex(
|
|
63
|
+
listRef: { current: any[] },
|
|
64
|
+
disabledIndices: number[] | ((index: number) => boolean) | undefined,
|
|
65
|
+
): number {
|
|
66
|
+
return findNonDisabledListIndex(listRef, { disabledIndices });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getMaxListIndex(
|
|
70
|
+
listRef: { current: any[] },
|
|
71
|
+
disabledIndices: number[] | ((index: number) => boolean) | undefined,
|
|
72
|
+
): number {
|
|
73
|
+
return findNonDisabledListIndex(listRef, {
|
|
74
|
+
decrement: true,
|
|
75
|
+
startingIndex: listRef.current.length,
|
|
76
|
+
disabledIndices,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/internals/composite/item/useCompositeItem.ts.
|
|
2
|
+
// Gives a composite item its roving `tabIndex` (0 when highlighted, -1 otherwise) and the
|
|
3
|
+
// focus/hover handlers that move the highlight. `useMergedRefs` → octane `useComposedRefs`.
|
|
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
|
+
import { useComposedRefs } from '../composeRefs';
|
|
10
|
+
import { useCompositeRootContext } from '../CompositeRootContext';
|
|
11
|
+
import { useCompositeListItem, type UseCompositeListItemParameters } from './useCompositeListItem';
|
|
12
|
+
|
|
13
|
+
export interface UseCompositeItemParameters<Metadata> extends Pick<
|
|
14
|
+
UseCompositeListItemParameters<Metadata>,
|
|
15
|
+
'metadata' | 'indexGuessBehavior'
|
|
16
|
+
> {}
|
|
17
|
+
|
|
18
|
+
export function useCompositeItem<Metadata>(...args: any[]): {
|
|
19
|
+
compositeProps: Record<string, any>;
|
|
20
|
+
compositeRef: (node: HTMLElement | null) => void;
|
|
21
|
+
index: number;
|
|
22
|
+
} {
|
|
23
|
+
const [user, slotArg] = splitSlot(args);
|
|
24
|
+
const slot = slotArg ?? S('useCompositeItem');
|
|
25
|
+
const params = (user[0] as UseCompositeItemParameters<Metadata>) ?? {};
|
|
26
|
+
|
|
27
|
+
const { highlightItemOnHover, highlightedIndex, onHighlightedIndexChange } =
|
|
28
|
+
useCompositeRootContext();
|
|
29
|
+
const { ref, index } = useCompositeListItem(params, subSlot(slot, 'li'));
|
|
30
|
+
|
|
31
|
+
const isHighlighted = highlightedIndex === index;
|
|
32
|
+
|
|
33
|
+
const itemRef = useRef<HTMLElement | null>(null, subSlot(slot, 'itemRef'));
|
|
34
|
+
const mergedRef = useComposedRefs(ref, itemRef, subSlot(slot, 'merged'));
|
|
35
|
+
|
|
36
|
+
const compositeProps: Record<string, any> = {
|
|
37
|
+
tabIndex: isHighlighted ? 0 : -1,
|
|
38
|
+
onFocus() {
|
|
39
|
+
onHighlightedIndexChange(index);
|
|
40
|
+
},
|
|
41
|
+
onMouseMove() {
|
|
42
|
+
const item = itemRef.current;
|
|
43
|
+
if (!highlightItemOnHover || !item) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const disabled = item.hasAttribute('disabled') || (item as any).ariaDisabled === 'true';
|
|
47
|
+
if (!isHighlighted && !disabled) {
|
|
48
|
+
item.focus();
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
compositeProps,
|
|
55
|
+
compositeRef: mergedRef as (node: HTMLElement | null) => void,
|
|
56
|
+
index,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/internals/composite/list/useCompositeListItem.ts.
|
|
2
|
+
// Registers a list item with the CompositeList and resolves its DOM-order `index` (via the
|
|
3
|
+
// list's `subscribeMapChange`), stashing the element in `elementsRef[index]`.
|
|
4
|
+
//
|
|
5
|
+
// SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
6
|
+
import { useCallback, useLayoutEffect, useRef, useState } from 'octane';
|
|
7
|
+
|
|
8
|
+
import { S, splitSlot, subSlot } from '../../internal';
|
|
9
|
+
import { useCompositeListContext } from './CompositeListContext';
|
|
10
|
+
|
|
11
|
+
export enum IndexGuessBehavior {
|
|
12
|
+
None,
|
|
13
|
+
GuessFromOrder,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UseCompositeListItemParameters<Metadata> {
|
|
17
|
+
index?: number;
|
|
18
|
+
label?: string | null;
|
|
19
|
+
metadata?: Metadata;
|
|
20
|
+
textRef?: { current: HTMLElement | null };
|
|
21
|
+
indexGuessBehavior?: IndexGuessBehavior;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UseCompositeListItemReturnValue {
|
|
25
|
+
ref: (node: HTMLElement | null) => void;
|
|
26
|
+
index: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useCompositeListItem<Metadata>(...args: any[]): UseCompositeListItemReturnValue {
|
|
30
|
+
const [user, slotArg] = splitSlot(args);
|
|
31
|
+
const slot = slotArg ?? S('useCompositeListItem');
|
|
32
|
+
const params = (user[0] as UseCompositeListItemParameters<Metadata>) ?? {};
|
|
33
|
+
const { label, metadata, textRef, indexGuessBehavior, index: externalIndex } = params;
|
|
34
|
+
|
|
35
|
+
const { register, unregister, subscribeMapChange, elementsRef, labelsRef, nextIndexRef } =
|
|
36
|
+
useCompositeListContext();
|
|
37
|
+
|
|
38
|
+
const indexRef = useRef(-1, subSlot(slot, 'idxRef'));
|
|
39
|
+
const [index, setIndex] = useState<number>(
|
|
40
|
+
externalIndex ??
|
|
41
|
+
(indexGuessBehavior === IndexGuessBehavior.GuessFromOrder
|
|
42
|
+
? () => {
|
|
43
|
+
if (indexRef.current === -1) {
|
|
44
|
+
const newIndex = nextIndexRef.current;
|
|
45
|
+
nextIndexRef.current += 1;
|
|
46
|
+
indexRef.current = newIndex;
|
|
47
|
+
}
|
|
48
|
+
return indexRef.current;
|
|
49
|
+
}
|
|
50
|
+
: -1),
|
|
51
|
+
subSlot(slot, 'idx'),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const componentRef = useRef<Element | null>(null, subSlot(slot, 'node'));
|
|
55
|
+
|
|
56
|
+
const ref = useCallback(
|
|
57
|
+
(node: HTMLElement | null) => {
|
|
58
|
+
componentRef.current = node;
|
|
59
|
+
if (index !== -1 && node !== null) {
|
|
60
|
+
elementsRef.current[index] = node;
|
|
61
|
+
if (labelsRef) {
|
|
62
|
+
const isLabelDefined = label !== undefined;
|
|
63
|
+
labelsRef.current[index] = isLabelDefined
|
|
64
|
+
? (label as string | null)
|
|
65
|
+
: (textRef?.current?.textContent ?? node.textContent);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
[index, elementsRef, labelsRef, label, textRef],
|
|
70
|
+
subSlot(slot, 'ref'),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
useLayoutEffect(
|
|
74
|
+
() => {
|
|
75
|
+
if (externalIndex != null) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const node = componentRef.current;
|
|
79
|
+
if (node) {
|
|
80
|
+
register(node, metadata as Metadata);
|
|
81
|
+
return () => {
|
|
82
|
+
unregister(node);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
},
|
|
87
|
+
[externalIndex, register, unregister, metadata],
|
|
88
|
+
subSlot(slot, 'e:reg'),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
useLayoutEffect(
|
|
92
|
+
() => {
|
|
93
|
+
if (externalIndex != null) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
return subscribeMapChange((map) => {
|
|
97
|
+
const i = componentRef.current ? map.get(componentRef.current)?.index : null;
|
|
98
|
+
if (i != null) {
|
|
99
|
+
setIndex(i);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
[externalIndex, subscribeMapChange, setIndex],
|
|
104
|
+
subSlot(slot, 'e:sub'),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return { ref, index };
|
|
108
|
+
}
|