@octanejs/floating-ui 0.1.9 → 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.
@@ -1,13 +1,31 @@
1
1
  // Ported from @floating-ui/react/utils. The DOM/grid/tabbable helpers are
2
- // framework-agnostic and copied ~verbatim; the three React hooks (useLatestRef,
3
- // useEffectEvent, useModernLayoutEffect) become octane hooks that take an explicit
4
- // slot (forwarded by their callers via subSlot — see ../internal).
2
+ // framework-agnostic and copied ~verbatim (signatures mirror upstream's
3
+ // `@floating-ui/react/utils` typings, with React event/ref types replaced by the
4
+ // native-DOM/octane analogs); the three React hooks (useLatestRef, useEffectEvent,
5
+ // useModernLayoutEffect) become octane hooks that take an explicit slot (forwarded
6
+ // by their callers via subSlot — see ../internal).
5
7
  import { isShadowRoot, isHTMLElement } from '@floating-ui/utils/dom';
6
8
  import { floor } from '@floating-ui/utils';
7
- import { tabbable } from 'tabbable';
9
+ import { tabbable, type FocusableElement } from 'tabbable';
8
10
  import { useCallback, useLayoutEffect, useRef } from 'octane';
11
+ import type { Octane } from 'octane/jsx-runtime';
12
+ import type { Dimensions } from '@floating-ui/dom';
9
13
 
10
14
  import { subSlot } from '../internal';
15
+ import type { Delay, FloatingNodeType, MutableRefObject, ReferenceType } from '../types';
16
+
17
+ /**
18
+ * The OBJECT form of octane's `style` prop — the port's analog of
19
+ * `React.CSSProperties` (octane's `style` additionally accepts a plain string;
20
+ * helpers that merge/inspect style objects require this form).
21
+ */
22
+ export type CSSProperties = Exclude<
23
+ Octane.HTMLAttributes<HTMLElement>['style'],
24
+ string | undefined
25
+ >;
26
+
27
+ /** Which list items are disabled (mirrors upstream's unexported `DisabledIndices`). */
28
+ export type DisabledIndices = Array<number> | ((index: number) => boolean);
11
29
 
12
30
  export function getPlatform(): string {
13
31
  const uaData = (navigator as any).userAgentData;
@@ -43,7 +61,7 @@ export function isMacSafari(): boolean {
43
61
  export function createAttribute(name: string): string {
44
62
  return 'data-floating-ui-' + name;
45
63
  }
46
- export function clearTimeoutIfSet(timeoutRef: { current: number }): void {
64
+ export function clearTimeoutIfSet(timeoutRef: MutableRefObject<number>): void {
47
65
  if (timeoutRef.current !== -1) {
48
66
  clearTimeout(timeoutRef.current);
49
67
  timeoutRef.current = -1;
@@ -85,36 +103,39 @@ export function contains(parent?: Element | null, child?: Element | null): boole
85
103
  }
86
104
  return false;
87
105
  }
88
- export function getTarget(event: any): EventTarget | null {
106
+ export function getTarget(event: Event): EventTarget | null {
89
107
  if ('composedPath' in event) {
90
108
  return event.composedPath()[0];
91
109
  }
92
- return event.target;
110
+ // Old-browser fallback: `lib.dom` types every Event with `composedPath`, so
111
+ // the false branch narrows `event` to `never` — widen for the legacy read.
112
+ return (event as Event).target;
93
113
  }
94
- export function isEventTargetWithin(event: any, node: any): boolean {
114
+ export function isEventTargetWithin(event: Event, node: Node | null | undefined): boolean {
95
115
  if (node == null) {
96
116
  return false;
97
117
  }
98
118
  if ('composedPath' in event) {
99
119
  return event.composedPath().includes(node);
100
120
  }
101
- const e = event;
102
- return e.target != null && node.contains(e.target);
121
+ // Old-browser fallback (see getTarget).
122
+ const e = event as Event;
123
+ return e.target != null && node.contains(e.target as Node);
103
124
  }
104
125
  export function isRootElement(element: Element): boolean {
105
126
  return element.matches('html,body');
106
127
  }
107
- export function getDocument(node: any): Document {
128
+ export function getDocument(node?: Element | null): Document {
108
129
  return node?.ownerDocument || document;
109
130
  }
110
- export function isTypeableElement(element: any): boolean {
131
+ export function isTypeableElement(element: unknown): boolean {
111
132
  return isHTMLElement(element) && element.matches(TYPEABLE_SELECTOR);
112
133
  }
113
- export function isTypeableCombobox(element: any): boolean {
134
+ export function isTypeableCombobox(element: Element | null): boolean {
114
135
  if (!element) return false;
115
136
  return element.getAttribute('role') === 'combobox' && isTypeableElement(element);
116
137
  }
117
- export function matchesFocusVisible(element: any): boolean {
138
+ export function matchesFocusVisible(element: Element | null): boolean {
118
139
  if (!element || isJSDOM()) return true;
119
140
  try {
120
141
  return element.matches(':focus-visible');
@@ -122,16 +143,23 @@ export function matchesFocusVisible(element: any): boolean {
122
143
  return true;
123
144
  }
124
145
  }
125
- export function getFloatingFocusElement(floatingElement: any): any {
146
+ export function getFloatingFocusElement(
147
+ floatingElement: HTMLElement | null | undefined,
148
+ ): HTMLElement | null {
126
149
  if (!floatingElement) {
127
150
  return null;
128
151
  }
129
152
  return floatingElement.hasAttribute(FOCUSABLE_ATTRIBUTE)
130
153
  ? floatingElement
131
- : floatingElement.querySelector('[' + FOCUSABLE_ATTRIBUTE + ']') || floatingElement;
154
+ : floatingElement.querySelector<HTMLElement>('[' + FOCUSABLE_ATTRIBUTE + ']') ||
155
+ floatingElement;
132
156
  }
133
157
 
134
- export function getNodeChildren(nodes: any[], id: any, onlyOpenChildren = true): any[] {
158
+ export function getNodeChildren<RT extends ReferenceType = ReferenceType>(
159
+ nodes: Array<FloatingNodeType<RT>>,
160
+ id: string | undefined,
161
+ onlyOpenChildren = true,
162
+ ): Array<FloatingNodeType<RT>> {
135
163
  const directChildren = nodes.filter(
136
164
  (node) => node.parentId === id && (!onlyOpenChildren || node.context?.open),
137
165
  );
@@ -140,10 +168,13 @@ export function getNodeChildren(nodes: any[], id: any, onlyOpenChildren = true):
140
168
  ...getNodeChildren(nodes, child.id, onlyOpenChildren),
141
169
  ]);
142
170
  }
143
- export function getDeepestNode(nodes: any[], id: any): any {
144
- let deepestNodeId: any;
171
+ export function getDeepestNode<RT extends ReferenceType = ReferenceType>(
172
+ nodes: Array<FloatingNodeType<RT>>,
173
+ id: string | undefined,
174
+ ): FloatingNodeType<RT> | undefined {
175
+ let deepestNodeId: string | undefined;
145
176
  let maxDepth = -1;
146
- function findDeepest(nodeId: any, depth: number) {
177
+ function findDeepest(nodeId: string | undefined, depth: number) {
147
178
  if (depth > maxDepth) {
148
179
  deepestNodeId = nodeId;
149
180
  maxDepth = depth;
@@ -156,8 +187,11 @@ export function getDeepestNode(nodes: any[], id: any): any {
156
187
  findDeepest(id, 0);
157
188
  return nodes.find((node) => node.id === deepestNodeId);
158
189
  }
159
- export function getNodeAncestors(nodes: any[], id: any): any[] {
160
- let allAncestors: any[] = [];
190
+ export function getNodeAncestors<RT extends ReferenceType = ReferenceType>(
191
+ nodes: Array<FloatingNodeType<RT>>,
192
+ id: string | undefined,
193
+ ): Array<FloatingNodeType<RT>> {
194
+ let allAncestors: Array<FloatingNodeType<RT>> = [];
161
195
  let currentParentId = nodes.find((node) => node.id === id)?.parentId;
162
196
  while (currentParentId) {
163
197
  const currentNode = nodes.find((node) => node.id === currentParentId);
@@ -169,7 +203,7 @@ export function getNodeAncestors(nodes: any[], id: any): any[] {
169
203
  return allAncestors;
170
204
  }
171
205
 
172
- export function stopEvent(event: any): void {
206
+ export function stopEvent(event: Event): void {
173
207
  event.preventDefault();
174
208
  event.stopPropagation();
175
209
  }
@@ -177,16 +211,16 @@ export function isReactEvent(event: any): boolean {
177
211
  return 'nativeEvent' in event;
178
212
  }
179
213
 
180
- export function isVirtualClick(event: any): boolean {
181
- if (event.mozInputSource === 0 && event.isTrusted) {
214
+ export function isVirtualClick(event: MouseEvent | PointerEvent): boolean {
215
+ if ((event as any).mozInputSource === 0 && event.isTrusted) {
182
216
  return true;
183
217
  }
184
- if (isAndroid() && event.pointerType) {
218
+ if (isAndroid() && (event as PointerEvent).pointerType) {
185
219
  return event.type === 'click' && event.buttons === 1;
186
220
  }
187
- return event.detail === 0 && !event.pointerType;
221
+ return event.detail === 0 && !(event as PointerEvent).pointerType;
188
222
  }
189
- export function isVirtualPointerEvent(event: any): boolean {
223
+ export function isVirtualPointerEvent(event: PointerEvent): boolean {
190
224
  if (isJSDOM()) return false;
191
225
  return (
192
226
  (!isAndroid() && event.width === 0 && event.height === 0) ||
@@ -203,15 +237,19 @@ export function isVirtualPointerEvent(event: any): boolean {
203
237
  event.pointerType === 'touch')
204
238
  );
205
239
  }
206
- export function isMouseLikePointerType(pointerType: any, strict?: boolean): boolean {
207
- const values: any[] = ['mouse', 'pen'];
240
+ export function isMouseLikePointerType(pointerType: string | undefined, strict?: boolean): boolean {
241
+ const values: Array<string | undefined> = ['mouse', 'pen'];
208
242
  if (!strict) {
209
243
  values.push('', undefined);
210
244
  }
211
245
  return values.includes(pointerType);
212
246
  }
213
247
 
214
- export function getDelay(value: any, prop: string, pointerType?: any): any {
248
+ export function getDelay(
249
+ value: Delay | ((...args: any[]) => Delay) | undefined,
250
+ prop: 'open' | 'close',
251
+ pointerType?: string,
252
+ ): number | undefined {
215
253
  if (pointerType && !isMouseLikePointerType(pointerType)) {
216
254
  return 0;
217
255
  }
@@ -232,8 +270,13 @@ export function getDelay(value: any, prop: string, pointerType?: any): any {
232
270
  export function camelCaseToKebabCase(str: string): string {
233
271
  return str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, ($, ofs) => (ofs ? '-' : '') + $.toLowerCase());
234
272
  }
235
- export function execWithArgsOrReturn(valueOrFn: any, args: any): any {
236
- return typeof valueOrFn === 'function' ? valueOrFn(args) : valueOrFn;
273
+ export function execWithArgsOrReturn<Value, SidePlacement>(
274
+ valueOrFn: Value | ((args: SidePlacement) => Value),
275
+ args: SidePlacement,
276
+ ): Value {
277
+ return typeof valueOrFn === 'function'
278
+ ? (valueOrFn as (args: SidePlacement) => Value)(args)
279
+ : valueOrFn;
237
280
  }
238
281
 
239
282
  // Fork of `fast-deep-equal` (from @floating-ui/react-dom) — compares functions by
@@ -313,7 +356,7 @@ export function useModernLayoutEffect(
313
356
  }
314
357
  }
315
358
 
316
- export function useLatestRef<T>(value: T, slot: symbol | undefined): { current: T } {
359
+ export function useLatestRef<T>(value: T, slot: symbol | undefined): MutableRefObject<T> {
317
360
  const ref = useRef(value, subSlot(slot, 'lr:ref'));
318
361
  useModernLayoutEffect(
319
362
  () => {
@@ -352,20 +395,37 @@ export function useEffectEvent<T extends (...args: any[]) => any>(
352
395
  export function isDifferentGridRow(index: number, cols: number, prevRow: number): boolean {
353
396
  return Math.floor(index / cols) !== prevRow;
354
397
  }
355
- export function isIndexOutOfListBounds(listRef: any, index: number): boolean {
398
+ export function isIndexOutOfListBounds(
399
+ listRef: MutableRefObject<Array<HTMLElement | null>>,
400
+ index: number,
401
+ ): boolean {
356
402
  return index < 0 || index >= listRef.current.length;
357
403
  }
358
- export function getMinListIndex(listRef: any, disabledIndices: any): number {
404
+ export function getMinListIndex(
405
+ listRef: MutableRefObject<Array<HTMLElement | null>>,
406
+ disabledIndices: DisabledIndices | undefined,
407
+ ): number {
359
408
  return findNonDisabledListIndex(listRef, { disabledIndices });
360
409
  }
361
- export function getMaxListIndex(listRef: any, disabledIndices: any): number {
410
+ export function getMaxListIndex(
411
+ listRef: MutableRefObject<Array<HTMLElement | null>>,
412
+ disabledIndices: DisabledIndices | undefined,
413
+ ): number {
362
414
  return findNonDisabledListIndex(listRef, {
363
415
  decrement: true,
364
416
  startingIndex: listRef.current.length,
365
417
  disabledIndices,
366
418
  });
367
419
  }
368
- export function findNonDisabledListIndex(listRef: any, _temp?: any): number {
420
+ export function findNonDisabledListIndex(
421
+ listRef: MutableRefObject<Array<HTMLElement | null>>,
422
+ _temp?: {
423
+ startingIndex?: number;
424
+ decrement?: boolean;
425
+ disabledIndices?: DisabledIndices;
426
+ amount?: number;
427
+ },
428
+ ): number {
369
429
  const {
370
430
  startingIndex = -1,
371
431
  decrement = false,
@@ -382,7 +442,21 @@ export function findNonDisabledListIndex(listRef: any, _temp?: any): number {
382
442
  );
383
443
  return index;
384
444
  }
385
- export function getGridNavigatedIndex(listRef: any, _ref: any): number {
445
+ export function getGridNavigatedIndex(
446
+ listRef: MutableRefObject<Array<HTMLElement | null>>,
447
+ _ref: {
448
+ event: KeyboardEvent;
449
+ orientation: 'horizontal' | 'vertical' | 'both';
450
+ loop: boolean;
451
+ rtl: boolean;
452
+ cols: number;
453
+ disabledIndices: DisabledIndices | undefined;
454
+ minIndex: number;
455
+ maxIndex: number;
456
+ prevIndex: number;
457
+ stopEvent?: boolean;
458
+ },
459
+ ): number {
386
460
  const {
387
461
  event,
388
462
  orientation,
@@ -513,8 +587,13 @@ export function getGridNavigatedIndex(listRef: any, _ref: any): number {
513
587
  return nextIndex;
514
588
  }
515
589
 
516
- export function createGridCellMap(sizes: any[], cols: number, dense: boolean): any[] {
517
- const cellMap: any[] = [];
590
+ /** For each cell index, gets the item index that occupies that cell. */
591
+ export function createGridCellMap(
592
+ sizes: Dimensions[],
593
+ cols: number,
594
+ dense: boolean,
595
+ ): Array<number | undefined> {
596
+ const cellMap: Array<number | undefined> = [];
518
597
  let startIndex = 0;
519
598
  sizes.forEach((_ref2, index) => {
520
599
  const { width, height } = _ref2;
@@ -551,12 +630,13 @@ export function createGridCellMap(sizes: any[], cols: number, dense: boolean): a
551
630
  });
552
631
  return [...cellMap];
553
632
  }
633
+ /** Gets cell index of an item's corner or -1 when index is -1. */
554
634
  export function getGridCellIndexOfCorner(
555
635
  index: number,
556
- sizes: any[],
557
- cellMap: any[],
636
+ sizes: Dimensions[],
637
+ cellMap: Array<number | undefined>,
558
638
  cols: number,
559
- corner: string,
639
+ corner: 'tl' | 'tr' | 'bl' | 'br',
560
640
  ): number {
561
641
  if (index === -1) return -1;
562
642
  const firstCellIndex = cellMap.indexOf(index);
@@ -579,10 +659,18 @@ export function getGridCellIndexOfCorner(
579
659
  }
580
660
  return -1;
581
661
  }
582
- export function getGridCellIndices(indices: any[], cellMap: any[]): number[] {
662
+ /** Gets all cell indices that correspond to the specified indices. */
663
+ export function getGridCellIndices(
664
+ indices: Array<number | undefined>,
665
+ cellMap: Array<number | undefined>,
666
+ ): number[] {
583
667
  return cellMap.flatMap((index, cellIndex) => (indices.includes(index) ? [cellIndex] : []));
584
668
  }
585
- export function isListIndexDisabled(listRef: any, index: number, disabledIndices?: any): boolean {
669
+ export function isListIndexDisabled(
670
+ listRef: MutableRefObject<Array<HTMLElement | null>>,
671
+ index: number,
672
+ disabledIndices?: DisabledIndices,
673
+ ): boolean {
586
674
  if (typeof disabledIndices === 'function') {
587
675
  return disabledIndices(index);
588
676
  } else if (disabledIndices) {
@@ -596,14 +684,14 @@ export function isListIndexDisabled(listRef: any, index: number, disabledIndices
596
684
  );
597
685
  }
598
686
 
599
- export const getTabbableOptions = (): any => ({
687
+ export const getTabbableOptions = (): { getShadowRoot: true; displayCheck: 'full' | 'none' } => ({
600
688
  getShadowRoot: true,
601
689
  displayCheck:
602
690
  typeof ResizeObserver === 'function' && ResizeObserver.toString().includes('[native code]')
603
691
  ? 'full'
604
692
  : 'none',
605
693
  });
606
- function getTabbableIn(container: any, dir: number): any {
694
+ function getTabbableIn(container: HTMLElement, dir: 1 | -1): FocusableElement | undefined {
607
695
  const list = tabbable(container, getTabbableOptions());
608
696
  const len = list.length;
609
697
  if (len === 0) return;
@@ -612,17 +700,26 @@ function getTabbableIn(container: any, dir: number): any {
612
700
  const nextIndex = index === -1 ? (dir === 1 ? 0 : len - 1) : index + dir;
613
701
  return list[nextIndex];
614
702
  }
615
- export function getNextTabbable(referenceElement: any): any {
616
- return getTabbableIn(getDocument(referenceElement).body, 1) || referenceElement;
703
+ export function getNextTabbable(referenceElement: Element | null): FocusableElement | null {
704
+ return (
705
+ getTabbableIn(getDocument(referenceElement).body, 1) ||
706
+ (referenceElement as FocusableElement | null)
707
+ );
617
708
  }
618
- export function getPreviousTabbable(referenceElement: any): any {
619
- return getTabbableIn(getDocument(referenceElement).body, -1) || referenceElement;
709
+ export function getPreviousTabbable(referenceElement: Element | null): FocusableElement | null {
710
+ return (
711
+ getTabbableIn(getDocument(referenceElement).body, -1) ||
712
+ (referenceElement as FocusableElement | null)
713
+ );
620
714
  }
621
715
  let rafId = 0;
622
- export function enqueueFocus(el: any, options: any = {}): void {
716
+ export function enqueueFocus(
717
+ el: Element | FocusableElement | null | undefined,
718
+ options: { preventScroll?: boolean; cancelPrevious?: boolean; sync?: boolean } = {},
719
+ ): void {
623
720
  const { preventScroll = false, cancelPrevious = true, sync = false } = options;
624
721
  cancelPrevious && cancelAnimationFrame(rafId);
625
- const exec = () => el?.focus({ preventScroll });
722
+ const exec = () => (el as FocusableElement | null | undefined)?.focus({ preventScroll });
626
723
  if (sync) {
627
724
  exec();
628
725
  } else {
@@ -630,21 +727,21 @@ export function enqueueFocus(el: any, options: any = {}): void {
630
727
  }
631
728
  }
632
729
 
633
- export function isOutsideEvent(event: any, container?: any): boolean {
634
- const containerElement = container || event.currentTarget;
635
- const relatedTarget = event.relatedTarget;
730
+ export function isOutsideEvent(event: FocusEvent, container?: Element | null): boolean {
731
+ const containerElement = container || (event.currentTarget as Element | null);
732
+ const relatedTarget = event.relatedTarget as Element | null;
636
733
  return !relatedTarget || !contains(containerElement, relatedTarget);
637
734
  }
638
- export function disableFocusInside(container: any): void {
735
+ export function disableFocusInside(container: HTMLElement): void {
639
736
  const tabbableElements = tabbable(container, getTabbableOptions());
640
- tabbableElements.forEach((element: any) => {
737
+ tabbableElements.forEach((element) => {
641
738
  element.dataset.tabindex = element.getAttribute('tabindex') || '';
642
739
  element.setAttribute('tabindex', '-1');
643
740
  });
644
741
  }
645
- export function enableFocusInside(container: any): void {
646
- const elements = container.querySelectorAll('[data-tabindex]');
647
- elements.forEach((element: any) => {
742
+ export function enableFocusInside(container: HTMLElement): void {
743
+ const elements = container.querySelectorAll<HTMLElement>('[data-tabindex]');
744
+ elements.forEach((element) => {
648
745
  const tabindex = element.dataset.tabindex;
649
746
  delete element.dataset.tabindex;
650
747
  if (tabindex) {