@octanejs/floating-ui 0.1.2

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.
@@ -0,0 +1,59 @@
1
+ // Ported from @floating-ui/react useMergeRefs. Merges an array of refs (objects or
2
+ // callbacks) into a single callback ref. React hooks → octane hooks + sub-slots.
3
+ import { useCallback, useMemo, useRef } from 'octane';
4
+
5
+ import { splitSlot, subSlot } from './internal';
6
+
7
+ export function useMergeRefs(...args: any[]): any {
8
+ const [user, slot] = splitSlot(args);
9
+ const refs = (user[0] as any[]) ?? [];
10
+
11
+ const cleanupRef = useRef<any>(undefined, subSlot(slot, 'cleanup'));
12
+
13
+ const refEffect = useCallback(
14
+ (instance: any) => {
15
+ const cleanups = refs.map((ref) => {
16
+ if (ref == null) {
17
+ return;
18
+ }
19
+ if (typeof ref === 'function') {
20
+ const refCallback = ref;
21
+ const refCleanup = refCallback(instance);
22
+ return typeof refCleanup === 'function'
23
+ ? refCleanup
24
+ : () => {
25
+ refCallback(null);
26
+ };
27
+ }
28
+ ref.current = instance;
29
+ return () => {
30
+ ref.current = null;
31
+ };
32
+ });
33
+ return () => {
34
+ cleanups.forEach((refCleanup) => (refCleanup == null ? void 0 : refCleanup()));
35
+ };
36
+ },
37
+ refs,
38
+ subSlot(slot, 'effect'),
39
+ );
40
+
41
+ return useMemo(
42
+ () => {
43
+ if (refs.every((ref) => ref == null)) {
44
+ return null;
45
+ }
46
+ return (value: any) => {
47
+ if (cleanupRef.current) {
48
+ cleanupRef.current();
49
+ cleanupRef.current = undefined;
50
+ }
51
+ if (value != null) {
52
+ cleanupRef.current = refEffect(value);
53
+ }
54
+ };
55
+ },
56
+ refs,
57
+ subSlot(slot, 'memo'),
58
+ );
59
+ }
package/src/useRole.ts ADDED
@@ -0,0 +1,106 @@
1
+ // Ported from @floating-ui/react useRole — adds the base ARIA props to the
2
+ // reference + floating elements for a given role. `context` is the first arg;
3
+ // `props` is optional → splitSlot for the trailing slot.
4
+ import { useCallback, useMemo } from 'octane';
5
+
6
+ import { splitSlot, subSlot } from './internal';
7
+ import { useFloatingParentNodeId } from './tree';
8
+ import { useId } from './useId';
9
+ import { getFloatingFocusElement } from './utils';
10
+
11
+ const componentRoleToAriaRoleMap = new Map<string, string | false>([
12
+ ['select', 'listbox'],
13
+ ['combobox', 'listbox'],
14
+ ['label', false],
15
+ ]);
16
+
17
+ export function useRole(...args: any[]): any {
18
+ const [user, slot] = splitSlot(args);
19
+ const context = user[0];
20
+ const props = (user[1] as any) ?? {};
21
+
22
+ const open = context.open;
23
+ const elements = context.elements;
24
+ const defaultFloatingId = context.floatingId;
25
+
26
+ const enabled = props.enabled ?? true;
27
+ const role = props.role ?? 'dialog';
28
+
29
+ const defaultReferenceId = useId(subSlot(slot, 'refid'));
30
+ const referenceId = elements.domReference?.id || defaultReferenceId;
31
+ const floatingId = useMemo(
32
+ () => getFloatingFocusElement(elements.floating)?.id || defaultFloatingId,
33
+ [elements.floating, defaultFloatingId],
34
+ subSlot(slot, 'm:fid'),
35
+ );
36
+ const mapped = componentRoleToAriaRoleMap.get(role);
37
+ const ariaRole = mapped != null ? mapped : role;
38
+ const parentId = useFloatingParentNodeId();
39
+ const isNested = parentId != null;
40
+
41
+ const reference = useMemo(
42
+ () => {
43
+ if (ariaRole === 'tooltip' || role === 'label') {
44
+ return {
45
+ ['aria-' + (role === 'label' ? 'labelledby' : 'describedby')]: open
46
+ ? floatingId
47
+ : undefined,
48
+ };
49
+ }
50
+ return {
51
+ 'aria-expanded': open ? 'true' : 'false',
52
+ 'aria-haspopup': ariaRole === 'alertdialog' ? 'dialog' : ariaRole,
53
+ 'aria-controls': open ? floatingId : undefined,
54
+ ...(ariaRole === 'listbox' && { role: 'combobox' }),
55
+ ...(ariaRole === 'menu' && { id: referenceId }),
56
+ ...(ariaRole === 'menu' && isNested && { role: 'menuitem' }),
57
+ ...(role === 'select' && { 'aria-autocomplete': 'none' }),
58
+ ...(role === 'combobox' && { 'aria-autocomplete': 'list' }),
59
+ };
60
+ },
61
+ [ariaRole, floatingId, isNested, open, referenceId, role],
62
+ subSlot(slot, 'm:ref'),
63
+ );
64
+
65
+ const floating = useMemo(
66
+ () => {
67
+ const floatingProps: any = {
68
+ id: floatingId,
69
+ ...(ariaRole && { role: ariaRole }),
70
+ };
71
+ if (ariaRole === 'tooltip' || role === 'label') {
72
+ return floatingProps;
73
+ }
74
+ return {
75
+ ...floatingProps,
76
+ ...(ariaRole === 'menu' && { 'aria-labelledby': referenceId }),
77
+ };
78
+ },
79
+ [ariaRole, floatingId, referenceId, role],
80
+ subSlot(slot, 'm:flo'),
81
+ );
82
+
83
+ const item = useCallback(
84
+ (_ref: any) => {
85
+ const { active, selected } = _ref;
86
+ const commonProps: any = {
87
+ role: 'option',
88
+ ...(active && { id: floatingId + '-fui-option' }),
89
+ };
90
+ switch (role) {
91
+ case 'select':
92
+ case 'combobox':
93
+ return { ...commonProps, 'aria-selected': selected };
94
+ }
95
+ return {};
96
+ },
97
+ [floatingId, role],
98
+ subSlot(slot, 'cb:item'),
99
+ );
100
+
101
+ return useMemo(
102
+ () => (enabled ? { reference, floating, item } : {}),
103
+ [enabled, reference, floating, item],
104
+ subSlot(slot, 'm:ret'),
105
+ );
106
+ }
@@ -0,0 +1,168 @@
1
+ // Ported from @floating-ui/react useTypeahead — type-to-select over a list. Native
2
+ // keyboard events (no event.nativeEvent usage here).
3
+ import { useMemo, useRef } from 'octane';
4
+
5
+ import { splitSlot, subSlot } from './internal';
6
+ import {
7
+ clearTimeoutIfSet,
8
+ stopEvent,
9
+ useEffectEvent,
10
+ useLatestRef,
11
+ useModernLayoutEffect,
12
+ } from './utils';
13
+
14
+ export function useTypeahead(...args: any[]): any {
15
+ const [user, slot] = splitSlot(args);
16
+ const context = user[0];
17
+ const props = (user[1] as any) ?? {};
18
+
19
+ const open = context.open;
20
+ const dataRef = context.dataRef;
21
+
22
+ const listRef = props.listRef;
23
+ const activeIndex = props.activeIndex;
24
+ const unstableOnMatch = props.onMatch;
25
+ const unstableOnTypingChange = props.onTypingChange;
26
+ const enabled = props.enabled ?? true;
27
+ const findMatch = props.findMatch ?? null;
28
+ const resetMs = props.resetMs ?? 750;
29
+ const ignoreKeys = props.ignoreKeys ?? [];
30
+ const selectedIndex = props.selectedIndex ?? null;
31
+
32
+ const timeoutIdRef = useRef(-1, subSlot(slot, 'timeout'));
33
+ const stringRef = useRef('', subSlot(slot, 'string'));
34
+ const prevIndexRef = useRef(
35
+ (selectedIndex != null ? selectedIndex : activeIndex) ?? -1,
36
+ subSlot(slot, 'prev'),
37
+ );
38
+ const matchIndexRef = useRef<any>(null, subSlot(slot, 'match'));
39
+
40
+ const onMatch = useEffectEvent(unstableOnMatch, subSlot(slot, 'onmatch'));
41
+ const onTypingChange = useEffectEvent(unstableOnTypingChange, subSlot(slot, 'ontyping'));
42
+ const findMatchRef = useLatestRef(findMatch, subSlot(slot, 'find'));
43
+ const ignoreKeysRef = useLatestRef(ignoreKeys, subSlot(slot, 'ignore'));
44
+
45
+ useModernLayoutEffect(
46
+ () => {
47
+ if (open) {
48
+ clearTimeoutIfSet(timeoutIdRef);
49
+ matchIndexRef.current = null;
50
+ stringRef.current = '';
51
+ }
52
+ },
53
+ [open],
54
+ subSlot(slot, 'e:open'),
55
+ );
56
+
57
+ useModernLayoutEffect(
58
+ () => {
59
+ if (open && stringRef.current === '') {
60
+ prevIndexRef.current = (selectedIndex != null ? selectedIndex : activeIndex) ?? -1;
61
+ }
62
+ },
63
+ [open, selectedIndex, activeIndex],
64
+ subSlot(slot, 'e:sync'),
65
+ );
66
+
67
+ const setTypingChange = useEffectEvent(
68
+ (value: any) => {
69
+ if (value) {
70
+ if (!dataRef.current.typing) {
71
+ dataRef.current.typing = value;
72
+ onTypingChange(value);
73
+ }
74
+ } else {
75
+ if (dataRef.current.typing) {
76
+ dataRef.current.typing = value;
77
+ onTypingChange(value);
78
+ }
79
+ }
80
+ },
81
+ subSlot(slot, 'settyping'),
82
+ );
83
+
84
+ const onKeyDown = useEffectEvent(
85
+ (event: any) => {
86
+ function getMatchingIndex(list: any[], orderedList: any[], string: string) {
87
+ const str = findMatchRef.current
88
+ ? findMatchRef.current(orderedList, string)
89
+ : orderedList.find(
90
+ (text: any) => text?.toLocaleLowerCase().indexOf(string.toLocaleLowerCase()) === 0,
91
+ );
92
+ return str ? list.indexOf(str) : -1;
93
+ }
94
+ const listContent = listRef.current;
95
+ if (stringRef.current.length > 0 && stringRef.current[0] !== ' ') {
96
+ if (getMatchingIndex(listContent, listContent, stringRef.current) === -1) {
97
+ setTypingChange(false);
98
+ } else if (event.key === ' ') {
99
+ stopEvent(event);
100
+ }
101
+ }
102
+ if (
103
+ listContent == null ||
104
+ ignoreKeysRef.current.includes(event.key) ||
105
+ event.key.length !== 1 ||
106
+ event.ctrlKey ||
107
+ event.metaKey ||
108
+ event.altKey
109
+ ) {
110
+ return;
111
+ }
112
+ if (open && event.key !== ' ') {
113
+ stopEvent(event);
114
+ setTypingChange(true);
115
+ }
116
+
117
+ const allowRapidSuccessionOfFirstLetter = listContent.every((text: any) =>
118
+ text ? text[0]?.toLocaleLowerCase() !== text[1]?.toLocaleLowerCase() : true,
119
+ );
120
+
121
+ if (allowRapidSuccessionOfFirstLetter && stringRef.current === event.key) {
122
+ stringRef.current = '';
123
+ prevIndexRef.current = matchIndexRef.current;
124
+ }
125
+ stringRef.current += event.key;
126
+ clearTimeoutIfSet(timeoutIdRef);
127
+ timeoutIdRef.current = window.setTimeout(() => {
128
+ stringRef.current = '';
129
+ prevIndexRef.current = matchIndexRef.current;
130
+ setTypingChange(false);
131
+ }, resetMs);
132
+ const prevIndex = prevIndexRef.current;
133
+ const index = getMatchingIndex(
134
+ listContent,
135
+ [...listContent.slice((prevIndex || 0) + 1), ...listContent.slice(0, (prevIndex || 0) + 1)],
136
+ stringRef.current,
137
+ );
138
+ if (index !== -1) {
139
+ onMatch(index);
140
+ matchIndexRef.current = index;
141
+ } else if (event.key !== ' ') {
142
+ stringRef.current = '';
143
+ setTypingChange(false);
144
+ }
145
+ },
146
+ subSlot(slot, 'keydown'),
147
+ );
148
+
149
+ const reference = useMemo(() => ({ onKeyDown }), [onKeyDown], subSlot(slot, 'm:ref'));
150
+ const floating = useMemo(
151
+ () => ({
152
+ onKeyDown,
153
+ onKeyUp(event: any) {
154
+ if (event.key === ' ') {
155
+ setTypingChange(false);
156
+ }
157
+ },
158
+ }),
159
+ [onKeyDown, setTypingChange],
160
+ subSlot(slot, 'm:flo'),
161
+ );
162
+
163
+ return useMemo(
164
+ () => (enabled ? { reference, floating } : {}),
165
+ [enabled, reference, floating],
166
+ subSlot(slot, 'm:ret'),
167
+ );
168
+ }