@octanejs/floating-ui 0.1.8 → 0.1.10

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.
@@ -30,6 +30,150 @@ import {
30
30
  useLatestRef,
31
31
  useModernLayoutEffect,
32
32
  } from './utils';
33
+ import type { Dimensions } from '@floating-ui/dom';
34
+ import type { ElementProps, FloatingRootContext, MutableRefObject } from './types';
35
+
36
+ export interface UseListNavigationProps {
37
+ /**
38
+ * A ref that holds an array of list items.
39
+ * @default empty list
40
+ */
41
+ listRef: MutableRefObject<Array<HTMLElement | null>>;
42
+ /**
43
+ * The index of the currently active (focused or highlighted) item, which may
44
+ * or may not be selected.
45
+ * @default null
46
+ */
47
+ activeIndex: number | null;
48
+ /**
49
+ * A callback that is called when the user navigates to a new active item,
50
+ * passed in a new `activeIndex`.
51
+ */
52
+ onNavigate?: (activeIndex: number | null) => void;
53
+ /**
54
+ * Whether the Hook is enabled, including all internal Effects and event
55
+ * handlers.
56
+ * @default true
57
+ */
58
+ enabled?: boolean;
59
+ /**
60
+ * The currently selected item index, which may or may not be active.
61
+ * @default null
62
+ */
63
+ selectedIndex?: number | null;
64
+ /**
65
+ * Whether to focus the item upon opening the floating element. 'auto' infers
66
+ * what to do based on the input type (keyboard vs. pointer), while a boolean
67
+ * value will force the value.
68
+ * @default 'auto'
69
+ */
70
+ focusItemOnOpen?: boolean | 'auto';
71
+ /**
72
+ * Whether hovering an item synchronizes the focus.
73
+ * @default true
74
+ */
75
+ focusItemOnHover?: boolean;
76
+ /**
77
+ * Whether pressing an arrow key on the navigation’s main axis opens the
78
+ * floating element.
79
+ * @default true
80
+ */
81
+ openOnArrowKeyDown?: boolean;
82
+ /**
83
+ * By default elements with either a `disabled` or `aria-disabled` attribute
84
+ * are skipped in the list navigation — however, this requires the items to
85
+ * be rendered.
86
+ * This prop allows you to manually specify indices which should be disabled,
87
+ * overriding the default logic.
88
+ * For Windows-style select menus, where the menu does not open when
89
+ * navigating via arrow keys, specify an empty array.
90
+ * @default undefined
91
+ */
92
+ disabledIndices?: Array<number> | ((index: number) => boolean);
93
+ /**
94
+ * Determines whether focus can escape the list, such that nothing is selected
95
+ * after navigating beyond the boundary of the list. In some
96
+ * autocomplete/combobox components, this may be desired, as screen
97
+ * readers will return to the input.
98
+ * `loop` must be `true`.
99
+ * @default false
100
+ */
101
+ allowEscape?: boolean;
102
+ /**
103
+ * Determines whether focus should loop around when navigating past the first
104
+ * or last item.
105
+ * @default false
106
+ */
107
+ loop?: boolean;
108
+ /**
109
+ * If the list is nested within another one (e.g. a nested submenu), the
110
+ * navigation semantics change.
111
+ * @default false
112
+ */
113
+ nested?: boolean;
114
+ /**
115
+ * Allows to specify the orientation of the parent list, which is used to
116
+ * determine the direction of the navigation.
117
+ * This is useful when list navigation is used within a Composite,
118
+ * as the hook can't determine the orientation of the parent list automatically.
119
+ */
120
+ parentOrientation?: UseListNavigationProps['orientation'];
121
+ /**
122
+ * Whether the direction of the floating element’s navigation is in RTL
123
+ * layout.
124
+ * @default false
125
+ */
126
+ rtl?: boolean;
127
+ /**
128
+ * Whether the focus is virtual (using `aria-activedescendant`).
129
+ * Use this if you need focus to remain on the reference element
130
+ * (such as an input), but allow arrow keys to navigate list items.
131
+ * This is common in autocomplete listbox components.
132
+ * Your virtually-focused list items must have a unique `id` set on them.
133
+ * If you’re using a component role with the `useRole()` Hook, then an `id` is
134
+ * generated automatically.
135
+ * @default false
136
+ */
137
+ virtual?: boolean;
138
+ /**
139
+ * The orientation in which navigation occurs.
140
+ * @default 'vertical'
141
+ */
142
+ orientation?: 'vertical' | 'horizontal' | 'both';
143
+ /**
144
+ * Specifies how many columns the list has (i.e., it’s a grid). Use an
145
+ * orientation of 'horizontal' (e.g. for an emoji picker/date picker, where
146
+ * pressing ArrowRight or ArrowLeft can change rows), or 'both' (where the
147
+ * current row cannot be escaped with ArrowRight or ArrowLeft, only ArrowUp
148
+ * and ArrowDown).
149
+ * @default 1
150
+ */
151
+ cols?: number;
152
+ /**
153
+ * Whether to scroll the active item into view when navigating. The default
154
+ * value uses nearest options.
155
+ */
156
+ scrollItemIntoView?: boolean | ScrollIntoViewOptions;
157
+ /**
158
+ * When using virtual focus management, this holds a ref to the
159
+ * virtually-focused item. This allows nested virtual navigation to be
160
+ * enabled, and lets you know when a nested element is virtually focused from
161
+ * the root reference handling the events. Requires `FloatingTree` to be
162
+ * setup.
163
+ */
164
+ virtualItemRef?: MutableRefObject<HTMLElement | null>;
165
+ /**
166
+ * Only for `cols > 1`, specify sizes for grid items.
167
+ * `{ width: 2, height: 2 }` means an item is 2 columns wide and 2 rows tall.
168
+ */
169
+ itemSizes?: Dimensions[];
170
+ /**
171
+ * Only relevant for `cols > 1` and items with different sizes, specify if
172
+ * the grid is dense (as defined in the CSS spec for `grid-auto-flow`).
173
+ * @default false
174
+ */
175
+ dense?: boolean;
176
+ }
33
177
 
34
178
  const ARROW_UP = 'ArrowUp';
35
179
  const ARROW_DOWN = 'ArrowDown';
@@ -73,10 +217,20 @@ function isCrossOrientationCloseKey(key: string, orientation: any, rtl: boolean,
73
217
  return doSwitch(orientation, vertical, horizontal);
74
218
  }
75
219
 
76
- export function useListNavigation(...args: any[]): any {
220
+ /**
221
+ * Adds arrow key-based navigation of a list of items, either using real DOM
222
+ * focus or virtual focus.
223
+ * @see https://floating-ui.com/docs/useListNavigation
224
+ */
225
+ export function useListNavigation(
226
+ context: FloatingRootContext,
227
+ props: UseListNavigationProps,
228
+ slot?: symbol,
229
+ ): ElementProps;
230
+ export function useListNavigation(...args: any[]): ElementProps {
77
231
  const [user, slot] = splitSlot(args);
78
- const context = user[0];
79
- const props = (user[1] as any) ?? {};
232
+ const context = user[0] as FloatingRootContext;
233
+ const props = (user[1] as UseListNavigationProps) ?? {};
80
234
 
81
235
  const open = context.open;
82
236
  const onOpenChange = context.onOpenChange;
@@ -128,7 +282,7 @@ export function useListNavigation(...args: any[]): any {
128
282
  const typeableComboboxReference = isTypeableCombobox(elements.domReference);
129
283
  const focusItemOnOpenRef = useRef(focusItemOnOpen, subSlot(slot, 'fioo'));
130
284
  const indexRef = useRef(selectedIndex != null ? selectedIndex : -1, subSlot(slot, 'index'));
131
- const keyRef = useRef<any>(null, subSlot(slot, 'key'));
285
+ const keyRef = useRef<string | null>(null, subSlot(slot, 'key'));
132
286
  const isPointerModalityRef = useRef(true, subSlot(slot, 'pm'));
133
287
  const previousOnNavigateRef = useRef(onNavigate, subSlot(slot, 'ponav'));
134
288
  const previousMountedRef = useRef(!!elements.floating, subSlot(slot, 'pmount'));
@@ -139,8 +293,8 @@ export function useListNavigation(...args: any[]): any {
139
293
  const latestOpenRef = useLatestRef(open, subSlot(slot, 'lor'));
140
294
  const scrollItemIntoViewRef = useLatestRef(scrollItemIntoView, subSlot(slot, 'siir'));
141
295
  const selectedIndexRef = useLatestRef(selectedIndex, subSlot(slot, 'sir'));
142
- const [activeId, setActiveId] = useState<any>(undefined, subSlot(slot, 'aid'));
143
- const [virtualId, setVirtualId] = useState<any>(undefined, subSlot(slot, 'vid'));
296
+ const [activeId, setActiveId] = useState<string | undefined>(undefined, subSlot(slot, 'aid'));
297
+ const [virtualId, setVirtualId] = useState<string | undefined>(undefined, subSlot(slot, 'vid'));
144
298
 
145
299
  const focusItem = useEffectEvent(
146
300
  () => {
@@ -436,7 +590,7 @@ export function useListNavigation(...args: any[]): any {
436
590
  const minGridIndex = cellMap.findIndex(
437
591
  (index) => index != null && !isListIndexDisabled(listRef, index, disabledIndices),
438
592
  );
439
- const maxGridIndex = cellMap.reduce(
593
+ const maxGridIndex = cellMap.reduce<number>(
440
594
  (foundIndex, index, cellIndex) =>
441
595
  index != null && !isListIndexDisabled(listRef, index, disabledIndices)
442
596
  ? cellIndex
@@ -713,7 +867,7 @@ export function useListNavigation(...args: any[]): any {
713
867
  subSlot(slot, 'm:ref'),
714
868
  );
715
869
 
716
- return useMemo(
870
+ return useMemo<ElementProps>(
717
871
  () => (enabled ? { reference, floating, item } : {}),
718
872
  [enabled, reference, floating, item],
719
873
  subSlot(slot, 'm:ret'),
@@ -3,12 +3,17 @@
3
3
  import { useCallback, useMemo, useRef } from 'octane';
4
4
 
5
5
  import { splitSlot, subSlot } from './internal';
6
+ import type { MutableRefObject, RefCallback } from './types';
6
7
 
8
+ export function useMergeRefs<Instance>(
9
+ refs: Array<MutableRefObject<Instance | null> | RefCallback<Instance> | null | undefined>,
10
+ slot?: symbol,
11
+ ): ((node: Instance | null) => void) | null;
7
12
  export function useMergeRefs(...args: any[]): any {
8
13
  const [user, slot] = splitSlot(args);
9
14
  const refs = (user[0] as any[]) ?? [];
10
15
 
11
- const cleanupRef = useRef<any>(undefined, subSlot(slot, 'cleanup'));
16
+ const cleanupRef = useRef<(() => void) | undefined>(undefined, subSlot(slot, 'cleanup'));
12
17
 
13
18
  const refEffect = useCallback(
14
19
  (instance: any) => {
package/src/useRole.ts CHANGED
@@ -7,17 +7,45 @@ import { splitSlot, subSlot } from './internal';
7
7
  import { useFloatingParentNodeId } from './tree';
8
8
  import { useId } from './useId';
9
9
  import { getFloatingFocusElement } from './utils';
10
+ import type { ElementProps, ExtendedUserProps, FloatingRootContext, HTMLProps } from './types';
10
11
 
11
- const componentRoleToAriaRoleMap = new Map<string, string | false>([
12
+ type AriaRole = 'tooltip' | 'dialog' | 'alertdialog' | 'menu' | 'listbox' | 'grid' | 'tree';
13
+ type ComponentRole = 'select' | 'label' | 'combobox';
14
+
15
+ export interface UseRoleProps {
16
+ /**
17
+ * Whether the Hook is enabled, including all internal Effects and event
18
+ * handlers.
19
+ * @default true
20
+ */
21
+ enabled?: boolean;
22
+ /**
23
+ * The role of the floating element.
24
+ * @default 'dialog'
25
+ */
26
+ role?: AriaRole | ComponentRole;
27
+ }
28
+
29
+ const componentRoleToAriaRoleMap = new Map<AriaRole | ComponentRole, AriaRole | false>([
12
30
  ['select', 'listbox'],
13
31
  ['combobox', 'listbox'],
14
32
  ['label', false],
15
33
  ]);
16
34
 
17
- export function useRole(...args: any[]): any {
35
+ /**
36
+ * Adds base screen reader props to the reference and floating elements for a
37
+ * given floating element `role`.
38
+ * @see https://floating-ui.com/docs/useRole
39
+ */
40
+ export function useRole(
41
+ context: FloatingRootContext,
42
+ props?: UseRoleProps,
43
+ slot?: symbol,
44
+ ): ElementProps;
45
+ export function useRole(...args: any[]): ElementProps {
18
46
  const [user, slot] = splitSlot(args);
19
- const context = user[0];
20
- const props = (user[1] as any) ?? {};
47
+ const context = user[0] as FloatingRootContext;
48
+ const props = (user[1] as UseRoleProps) ?? {};
21
49
 
22
50
  const open = context.open;
23
51
  const elements = context.elements;
@@ -34,18 +62,19 @@ export function useRole(...args: any[]): any {
34
62
  subSlot(slot, 'm:fid'),
35
63
  );
36
64
  const mapped = componentRoleToAriaRoleMap.get(role);
37
- const ariaRole = mapped != null ? mapped : role;
65
+ // The map covers every ComponentRole, so the fallback `role` is an AriaRole.
66
+ const ariaRole = (mapped != null ? mapped : role) as AriaRole | false;
38
67
  const parentId = useFloatingParentNodeId();
39
68
  const isNested = parentId != null;
40
69
 
41
- const reference = useMemo(
70
+ const reference = useMemo<HTMLProps<Element>>(
42
71
  () => {
43
72
  if (ariaRole === 'tooltip' || role === 'label') {
44
73
  return {
45
74
  ['aria-' + (role === 'label' ? 'labelledby' : 'describedby')]: open
46
75
  ? floatingId
47
76
  : undefined,
48
- };
77
+ } as HTMLProps<Element>;
49
78
  }
50
79
  return {
51
80
  'aria-expanded': open ? 'true' : 'false',
@@ -62,9 +91,9 @@ export function useRole(...args: any[]): any {
62
91
  subSlot(slot, 'm:ref'),
63
92
  );
64
93
 
65
- const floating = useMemo(
94
+ const floating = useMemo<HTMLProps<HTMLElement>>(
66
95
  () => {
67
- const floatingProps: any = {
96
+ const floatingProps: HTMLProps<HTMLElement> = {
68
97
  id: floatingId,
69
98
  ...(ariaRole && { role: ariaRole }),
70
99
  };
@@ -81,9 +110,9 @@ export function useRole(...args: any[]): any {
81
110
  );
82
111
 
83
112
  const item = useCallback(
84
- (_ref: any) => {
113
+ (_ref: ExtendedUserProps): HTMLProps<HTMLElement> => {
85
114
  const { active, selected } = _ref;
86
- const commonProps: any = {
115
+ const commonProps: HTMLProps<HTMLElement> = {
87
116
  role: 'option',
88
117
  ...(active && { id: floatingId + '-fui-option' }),
89
118
  };
@@ -98,7 +127,7 @@ export function useRole(...args: any[]): any {
98
127
  subSlot(slot, 'cb:item'),
99
128
  );
100
129
 
101
- return useMemo(
130
+ return useMemo<ElementProps>(
102
131
  () => (enabled ? { reference, floating, item } : {}),
103
132
  [enabled, reference, floating, item],
104
133
  subSlot(slot, 'm:ret'),
@@ -10,11 +10,72 @@ import {
10
10
  useLatestRef,
11
11
  useModernLayoutEffect,
12
12
  } from './utils';
13
+ import type { ElementProps, FloatingRootContext, MutableRefObject } from './types';
13
14
 
14
- export function useTypeahead(...args: any[]): any {
15
+ export interface UseTypeaheadProps {
16
+ /**
17
+ * A ref which contains an array of strings whose indices match the HTML
18
+ * elements of the list.
19
+ * @default empty list
20
+ */
21
+ listRef: MutableRefObject<Array<string | null>>;
22
+ /**
23
+ * The index of the active (focused or highlighted) item in the list.
24
+ * @default null
25
+ */
26
+ activeIndex: number | null;
27
+ /**
28
+ * Callback invoked with the matching index if found as the user types.
29
+ */
30
+ onMatch?: (index: number) => void;
31
+ /**
32
+ * Callback invoked with the typing state as the user types.
33
+ */
34
+ onTypingChange?: (isTyping: boolean) => void;
35
+ /**
36
+ * Whether the Hook is enabled, including all internal Effects and event
37
+ * handlers.
38
+ * @default true
39
+ */
40
+ enabled?: boolean;
41
+ /**
42
+ * A function that returns the matching string from the list.
43
+ * @default lowercase-finder
44
+ */
45
+ findMatch?:
46
+ | null
47
+ | ((list: Array<string | null>, typedString: string) => string | null | undefined);
48
+ /**
49
+ * The number of milliseconds to wait before resetting the typed string.
50
+ * @default 750
51
+ */
52
+ resetMs?: number;
53
+ /**
54
+ * An array of keys to ignore when typing.
55
+ * @default []
56
+ */
57
+ ignoreKeys?: Array<string>;
58
+ /**
59
+ * The index of the selected item in the list, if available.
60
+ * @default null
61
+ */
62
+ selectedIndex?: number | null;
63
+ }
64
+
65
+ /**
66
+ * Provides a matching callback that can be used to focus an item as the user
67
+ * types, often used in tandem with `useListNavigation()`.
68
+ * @see https://floating-ui.com/docs/useTypeahead
69
+ */
70
+ export function useTypeahead(
71
+ context: FloatingRootContext,
72
+ props: UseTypeaheadProps,
73
+ slot?: symbol,
74
+ ): ElementProps;
75
+ export function useTypeahead(...args: any[]): ElementProps {
15
76
  const [user, slot] = splitSlot(args);
16
- const context = user[0];
17
- const props = (user[1] as any) ?? {};
77
+ const context = user[0] as FloatingRootContext;
78
+ const props = (user[1] as UseTypeaheadProps) ?? {};
18
79
 
19
80
  const open = context.open;
20
81
  const dataRef = context.dataRef;
@@ -31,11 +92,11 @@ export function useTypeahead(...args: any[]): any {
31
92
 
32
93
  const timeoutIdRef = useRef(-1, subSlot(slot, 'timeout'));
33
94
  const stringRef = useRef('', subSlot(slot, 'string'));
34
- const prevIndexRef = useRef(
95
+ const prevIndexRef = useRef<number | null>(
35
96
  (selectedIndex != null ? selectedIndex : activeIndex) ?? -1,
36
97
  subSlot(slot, 'prev'),
37
98
  );
38
- const matchIndexRef = useRef<any>(null, subSlot(slot, 'match'));
99
+ const matchIndexRef = useRef<number | null>(null, subSlot(slot, 'match'));
39
100
 
40
101
  const onMatch = useEffectEvent(unstableOnMatch, subSlot(slot, 'onmatch'));
41
102
  const onTypingChange = useEffectEvent(unstableOnTypingChange, subSlot(slot, 'ontyping'));
@@ -65,7 +126,7 @@ export function useTypeahead(...args: any[]): any {
65
126
  );
66
127
 
67
128
  const setTypingChange = useEffectEvent(
68
- (value: any) => {
129
+ (value: boolean) => {
69
130
  if (value) {
70
131
  if (!dataRef.current.typing) {
71
132
  dataRef.current.typing = value;
@@ -82,8 +143,12 @@ export function useTypeahead(...args: any[]): any {
82
143
  );
83
144
 
84
145
  const onKeyDown = useEffectEvent(
85
- (event: any) => {
86
- function getMatchingIndex(list: any[], orderedList: any[], string: string) {
146
+ (event: KeyboardEvent) => {
147
+ function getMatchingIndex(
148
+ list: Array<string | null>,
149
+ orderedList: Array<string | null>,
150
+ string: string,
151
+ ) {
87
152
  const str = findMatchRef.current
88
153
  ? findMatchRef.current(orderedList, string)
89
154
  : orderedList.find(
@@ -150,7 +215,7 @@ export function useTypeahead(...args: any[]): any {
150
215
  const floating = useMemo(
151
216
  () => ({
152
217
  onKeyDown,
153
- onKeyUp(event: any) {
218
+ onKeyUp(event: KeyboardEvent) {
154
219
  if (event.key === ' ') {
155
220
  setTypingChange(false);
156
221
  }
@@ -160,7 +225,7 @@ export function useTypeahead(...args: any[]): any {
160
225
  subSlot(slot, 'm:flo'),
161
226
  );
162
227
 
163
- return useMemo(
228
+ return useMemo<ElementProps>(
164
229
  () => (enabled ? { reference, floating } : {}),
165
230
  [enabled, reference, floating],
166
231
  subSlot(slot, 'm:ret'),