@octanejs/base-ui 0.1.14 → 0.1.16

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.
Files changed (49) hide show
  1. package/package.json +5 -5
  2. package/src/alert-dialog.ts +2 -0
  3. package/src/button.ts +63 -0
  4. package/src/context-menu.ts +317 -0
  5. package/src/csp-provider.ts +37 -0
  6. package/src/dialog.ts +89 -22
  7. package/src/direction-provider.ts +32 -0
  8. package/src/index.ts +10 -0
  9. package/src/menu.ts +3387 -0
  10. package/src/menubar.ts +160 -0
  11. package/src/popover.ts +99 -25
  12. package/src/preview-card.ts +812 -0
  13. package/src/tooltip.ts +1053 -0
  14. package/src/unstable-use-media-query.ts +101 -0
  15. package/src/utils/CSPContext.ts +21 -0
  16. package/src/utils/ContextMenuRootContext.ts +44 -0
  17. package/src/utils/FloatingPortalLite.ts +29 -0
  18. package/src/utils/MenubarContext.ts +38 -0
  19. package/src/utils/RequestQueue.ts +132 -0
  20. package/src/utils/TimeoutManager.ts +32 -0
  21. package/src/utils/composite/grid-utils.ts +437 -0
  22. package/src/utils/composite/gridNavigation.ts +127 -0
  23. package/src/utils/composite/list-utils.ts +56 -28
  24. package/src/utils/composite/useCompositeRoot.ts +11 -13
  25. package/src/utils/constants.ts +22 -0
  26. package/src/utils/createChangeEventDetails.ts +4 -0
  27. package/src/utils/findRootOwnerId.ts +19 -0
  28. package/src/utils/floating/FloatingDelayGroup.ts +280 -0
  29. package/src/utils/floating/element.ts +36 -2
  30. package/src/utils/floating/gridNavigation.ts +54 -0
  31. package/src/utils/floating/safePolygon.ts +448 -0
  32. package/src/utils/floating/useClientPoint.ts +276 -0
  33. package/src/utils/floating/useFocus.ts +248 -0
  34. package/src/utils/floating/useHoverFloatingInteraction.ts +301 -11
  35. package/src/utils/floating/useHoverInteractionSharedState.ts +139 -0
  36. package/src/utils/floating/useHoverReferenceInteraction.ts +507 -14
  37. package/src/utils/floating/useHoverShared.ts +78 -0
  38. package/src/utils/floating/useListNavigation.ts +1018 -0
  39. package/src/utils/floating/useTypeahead.ts +287 -0
  40. package/src/utils/getCssDimensions.ts +28 -0
  41. package/src/utils/getPseudoElementBounds.ts +54 -0
  42. package/src/utils/platform.ts +10 -0
  43. package/src/utils/popups/index.ts +6 -0
  44. package/src/utils/popups/inlineRect.ts +289 -0
  45. package/src/utils/useMixedToggleClickHandler.ts +77 -0
  46. package/src/utils/useOpenInteractionType.ts +37 -1
  47. package/src/utils/usePopupAutoResize.ts +250 -0
  48. package/src/utils/usePopupViewport.ts +375 -0
  49. package/src/utils/usePreviousValue.ts +25 -0
@@ -0,0 +1,287 @@
1
+ // Ported from .base-ui/packages/react/src/floating-ui-react/hooks/useTypeahead.ts (v1.6.0). Matches
2
+ // list items as the user types, the way a native `<select>` does — usually paired with
3
+ // `useListNavigation`. Returns an `ElementProps` bag whose `onKeyDown`/`onBlur` the reference and
4
+ // the floating element both merge.
5
+ //
6
+ // octane adaptations: handlers receive the NATIVE event, so `event.nativeEvent` collapses to
7
+ // `event`; `useIsoLayoutEffect` is octane's `useLayoutEffect`; every composed hook threads an
8
+ // explicit slot.
9
+ //
10
+ // SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
11
+ import { useLayoutEffect, useMemo, useRef } from 'octane';
12
+
13
+ import { S, splitSlot, subSlot } from '../../internal';
14
+ import { EMPTY_ARRAY } from '../empty';
15
+ import { useStableCallback } from '../useStableCallback';
16
+ import { useTimeout } from '../useTimeout';
17
+ import { isListIndexDisabled, stopEvent, type DisabledIndices } from '../composite/list-utils';
18
+ import { isElementVisible } from './composite';
19
+ import { contains } from './element';
20
+ import type { ElementProps, FloatingContext, FloatingRootContext } from './types';
21
+
22
+ export interface UseTypeaheadProps {
23
+ /**
24
+ * A ref which contains an array of strings whose indices match the HTML
25
+ * elements of the list.
26
+ * @default empty list
27
+ */
28
+ listRef: { current: Array<string | null> };
29
+ /**
30
+ * The index of the active (focused or highlighted) item in the list.
31
+ * @default null
32
+ */
33
+ activeIndex: number | null;
34
+ /**
35
+ * Callback invoked with the matching index if found as the user types.
36
+ */
37
+ onMatch?: ((index: number) => void) | undefined;
38
+ /**
39
+ * Optional list of item elements that correspond to `listRef` indices.
40
+ * When an element exists for an index, typeahead skips it if it is hidden by
41
+ * `display: none`, `visibility: hidden|collapse`, or other
42
+ * browser-reported visibility checks.
43
+ */
44
+ elementsRef?: { current: Array<HTMLElement | null> } | undefined;
45
+ /**
46
+ * Indices that are disabled, either as an array or a predicate (the same shape as
47
+ * `useListNavigation`'s `disabledIndices`). Disabled items are skipped while matching,
48
+ * so a single keypress advances to the next selectable item (matching native `<select>`
49
+ * and arrow-key navigation). The disabled check doesn't read `elementsRef`, so consumers
50
+ * whose items stay mounted-but-hidden while closed can still skip disabled items without
51
+ * passing `elementsRef`.
52
+ */
53
+ disabledIndices?: DisabledIndices | undefined;
54
+ /**
55
+ * Callback invoked with the current typing activity as the user types.
56
+ */
57
+ onTyping?: ((isTyping: boolean) => void) | undefined;
58
+ /**
59
+ * Whether the hook is enabled, including all internal effects and event
60
+ * handlers.
61
+ * @default true
62
+ */
63
+ enabled?: boolean | undefined;
64
+ /**
65
+ * The number of milliseconds to wait before resetting the typed string.
66
+ * @default 750
67
+ */
68
+ resetMs?: number | undefined;
69
+ /**
70
+ * The index of the selected item in the list, if available.
71
+ * @default null
72
+ */
73
+ selectedIndex?: number | null | undefined;
74
+ }
75
+
76
+ /**
77
+ * Provides a matching callback that can be used to focus an item as the user
78
+ * types, often used in tandem with `useListNavigation()`.
79
+ * @see https://floating-ui.com/docs/useTypeahead
80
+ */
81
+ export function useTypeahead(...args: any[]): ElementProps {
82
+ const [user, slotArg] = splitSlot(args);
83
+ const slot = slotArg ?? S('useTypeahead');
84
+ const context = user[0] as FloatingRootContext | FloatingContext;
85
+ const props = user[1] as UseTypeaheadProps;
86
+
87
+ const {
88
+ listRef,
89
+ elementsRef,
90
+ activeIndex,
91
+ onMatch: onMatchProp,
92
+ disabledIndices,
93
+ onTyping,
94
+ enabled = true,
95
+ resetMs = 750,
96
+ selectedIndex = null,
97
+ } = props;
98
+
99
+ const store = (context && 'rootStore' in context ? context.rootStore : context) as any;
100
+
101
+ const open = store.useState('open', subSlot(slot, 'open'));
102
+
103
+ const timeout = useTimeout(subSlot(slot, 'to'));
104
+ const stringRef = useRef('', subSlot(slot, 'str'));
105
+ const prevIndexRef = useRef<number | null>(
106
+ selectedIndex ?? activeIndex ?? -1,
107
+ subSlot(slot, 'prev'),
108
+ );
109
+ const matchIndexRef = useRef<number | null>(null, subSlot(slot, 'match'));
110
+
111
+ const onKeyDown = useStableCallback(
112
+ (event: KeyboardEvent) => {
113
+ function isVisible(index: number) {
114
+ const element = elementsRef?.current[index];
115
+ return !element || isElementVisible(element);
116
+ }
117
+
118
+ function isItemAvailable(index: number) {
119
+ if (!isVisible(index)) {
120
+ return false;
121
+ }
122
+ // Visibility is handled above; pass an empty element list so `isListIndexDisabled`
123
+ // resolves only the explicit `disabledIndices` (array/predicate) and skips its own
124
+ // visibility/attribute fallbacks. Consumers that don't opt in keep matching every
125
+ // visible item.
126
+ return disabledIndices == null || !isListIndexDisabled(EMPTY_ARRAY, index, disabledIndices);
127
+ }
128
+
129
+ function getMatchingIndex(list: Array<string | null>, string: string, startIndex = 0) {
130
+ if (list.length === 0) {
131
+ return -1;
132
+ }
133
+
134
+ const normalizedStartIndex = ((startIndex % list.length) + list.length) % list.length;
135
+ const lowerString = string.toLowerCase();
136
+
137
+ for (let offset = 0; offset < list.length; offset += 1) {
138
+ const index = (normalizedStartIndex + offset) % list.length;
139
+ const text = list[index];
140
+ if (!text?.toLowerCase().startsWith(lowerString) || !isItemAvailable(index)) {
141
+ continue;
142
+ }
143
+ return index;
144
+ }
145
+ return -1;
146
+ }
147
+
148
+ const listContent = listRef.current;
149
+
150
+ if (stringRef.current.length > 0 && event.key === ' ') {
151
+ // Space should continue the in-progress typeahead session.
152
+ stopEvent(event);
153
+ onTyping?.(true);
154
+ }
155
+
156
+ if (stringRef.current.length > 0 && stringRef.current[0] !== ' ') {
157
+ if (getMatchingIndex(listContent, stringRef.current) === -1 && event.key !== ' ') {
158
+ onTyping?.(false);
159
+ }
160
+ }
161
+
162
+ if (
163
+ listContent == null ||
164
+ // Character key.
165
+ event.key.length !== 1 ||
166
+ // Modifier key.
167
+ event.ctrlKey ||
168
+ event.metaKey ||
169
+ event.altKey
170
+ ) {
171
+ return;
172
+ }
173
+
174
+ if (open && event.key !== ' ') {
175
+ stopEvent(event);
176
+ onTyping?.(true);
177
+ }
178
+
179
+ // Capture whether this is a new typing session before mutating the string.
180
+ const isNewSession = stringRef.current === '';
181
+ if (isNewSession) {
182
+ prevIndexRef.current = selectedIndex ?? activeIndex ?? -1;
183
+ }
184
+
185
+ // Bail out if the list contains a word like "llama" or "aaron". TODO:
186
+ // allow it in this case, too. Unavailable items are skipped while matching, so
187
+ // they must be ignored here as well — otherwise a hidden or disabled double-letter
188
+ // label would block rapid cycling through the available items.
189
+ const allowRapidSuccessionOfFirstLetter = listContent.every((text, index) =>
190
+ text && isItemAvailable(index) ? text[0]?.toLowerCase() !== text[1]?.toLowerCase() : true,
191
+ );
192
+
193
+ // Allows the user to cycle through items that start with the same letter
194
+ // in rapid succession.
195
+ if (allowRapidSuccessionOfFirstLetter && stringRef.current === event.key) {
196
+ stringRef.current = '';
197
+ prevIndexRef.current = matchIndexRef.current;
198
+ }
199
+
200
+ stringRef.current += event.key;
201
+ timeout.start(resetMs, () => {
202
+ stringRef.current = '';
203
+ prevIndexRef.current = matchIndexRef.current;
204
+ onTyping?.(false);
205
+ });
206
+
207
+ // Compute the starting index for this search.
208
+ // If this is a new typing session (string is empty), base it on the current
209
+ // selection/active item; otherwise continue from the last matched index.
210
+ const prevIndex = isNewSession ? (selectedIndex ?? activeIndex ?? -1) : prevIndexRef.current;
211
+ const startIndex = (prevIndex ?? 0) + 1;
212
+
213
+ const index = getMatchingIndex(listContent, stringRef.current, startIndex);
214
+
215
+ if (index !== -1) {
216
+ onMatchProp?.(index);
217
+ matchIndexRef.current = index;
218
+ } else if (event.key !== ' ') {
219
+ stringRef.current = '';
220
+ onTyping?.(false);
221
+ }
222
+ },
223
+ subSlot(slot, 'okd'),
224
+ );
225
+
226
+ const onBlur = useStableCallback(
227
+ (event: FocusEvent) => {
228
+ const next = event.relatedTarget as Element | null;
229
+ const currentDomReferenceElement = store.select('domReferenceElement');
230
+ const currentFloatingElement = store.select('floatingElement');
231
+ const withinComposite =
232
+ contains(currentDomReferenceElement, next) || contains(currentFloatingElement, next);
233
+
234
+ // Keep the session if focus moves within the composite (reference <-> floating).
235
+ if (withinComposite) {
236
+ return;
237
+ }
238
+
239
+ // End the current typing session when focus leaves the composite entirely.
240
+ timeout.clear();
241
+ stringRef.current = '';
242
+ prevIndexRef.current = matchIndexRef.current;
243
+ onTyping?.(false);
244
+ },
245
+ subSlot(slot, 'ob'),
246
+ );
247
+
248
+ useLayoutEffect(
249
+ () => {
250
+ if (!open && selectedIndex !== null) {
251
+ return;
252
+ }
253
+
254
+ timeout.clear();
255
+ matchIndexRef.current = null;
256
+
257
+ if (stringRef.current !== '') {
258
+ stringRef.current = '';
259
+ }
260
+ },
261
+ [open, selectedIndex, timeout],
262
+ subSlot(slot, 'e:reset'),
263
+ );
264
+
265
+ useLayoutEffect(
266
+ () => {
267
+ // Sync arrow key navigation but not typeahead navigation.
268
+ if (open && stringRef.current === '') {
269
+ prevIndexRef.current = selectedIndex ?? activeIndex ?? -1;
270
+ }
271
+ },
272
+ [open, selectedIndex, activeIndex],
273
+ subSlot(slot, 'e:sync'),
274
+ );
275
+
276
+ const sharedProps = useMemo(
277
+ () => ({ onKeyDown, onBlur }),
278
+ [onKeyDown, onBlur],
279
+ subSlot(slot, 'shared'),
280
+ );
281
+
282
+ return useMemo(
283
+ () => (enabled ? { reference: sharedProps, floating: sharedProps } : {}),
284
+ [enabled, sharedProps],
285
+ subSlot(slot, 'out'),
286
+ );
287
+ }
@@ -0,0 +1,28 @@
1
+ // Ported from .base-ui/packages/react/src/utils/getCssDimensions.ts (v1.6.0). Base UI reads this
2
+ // from `@floating-ui/utils`; `round` is inlined here rather than adding an import.
3
+ import { isHTMLElement } from './dom';
4
+
5
+ export interface Dimensions {
6
+ width: number;
7
+ height: number;
8
+ }
9
+
10
+ export function getCssDimensions(element: Element): Dimensions {
11
+ const css = getComputedStyle(element);
12
+ // In testing environments the `width`/`height` properties are empty strings for SVG elements,
13
+ // which would parse to NaN. Fall back to 0.
14
+ let width = parseFloat(css.width) || 0;
15
+ let height = parseFloat(css.height) || 0;
16
+ const hasOffset = isHTMLElement(element);
17
+ const offsetWidth = hasOffset ? (element as HTMLElement).offsetWidth : width;
18
+ const offsetHeight = hasOffset ? (element as HTMLElement).offsetHeight : height;
19
+ const shouldFallback =
20
+ Math.round(width * 2) / 2 !== offsetWidth || Math.round(height * 2) / 2 !== offsetHeight;
21
+
22
+ if (shouldFallback) {
23
+ width = offsetWidth;
24
+ height = offsetHeight;
25
+ }
26
+
27
+ return { width, height };
28
+ }
@@ -0,0 +1,54 @@
1
+ // Ported from .base-ui/packages/react/src/utils/getPseudoElementBounds.ts (v1.6.0). Returns an
2
+ // element's bounding box widened to cover its `::before`/`::after` pseudo-elements, so hit-testing
3
+ // (safe-polygon / mouse-tracking) accounts for decorations drawn outside the box.
4
+ //
5
+ // Pure — no renderer API is involved, so this is a straight transcription.
6
+ import { ownerWindow } from './owner';
7
+ import { platform } from './platform';
8
+
9
+ interface ElementBounds {
10
+ left: number;
11
+ right: number;
12
+ top: number;
13
+ bottom: number;
14
+ }
15
+
16
+ export function getPseudoElementBounds(element: HTMLElement): ElementBounds {
17
+ const elementRect = element.getBoundingClientRect();
18
+ const win = ownerWindow(element);
19
+
20
+ // Avoid "Not implemented: window.getComputedStyle(elt, pseudoElt)" in jsdom.
21
+ if (platform.env.jsdom) {
22
+ return elementRect;
23
+ }
24
+
25
+ const beforeStyles = win.getComputedStyle(element, '::before');
26
+ const afterStyles = win.getComputedStyle(element, '::after');
27
+
28
+ const hasPseudoElements = beforeStyles.content !== 'none' || afterStyles.content !== 'none';
29
+
30
+ if (!hasPseudoElements) {
31
+ return elementRect;
32
+ }
33
+
34
+ // Get dimensions of pseudo-elements
35
+ const beforeWidth = parseFloat(beforeStyles.width) || 0;
36
+ const beforeHeight = parseFloat(beforeStyles.height) || 0;
37
+ const afterWidth = parseFloat(afterStyles.width) || 0;
38
+ const afterHeight = parseFloat(afterStyles.height) || 0;
39
+
40
+ // Calculate max dimensions including pseudo-elements
41
+ const totalWidth = Math.max(elementRect.width, beforeWidth, afterWidth);
42
+ const totalHeight = Math.max(elementRect.height, beforeHeight, afterHeight);
43
+
44
+ // Calculate the differences to extend the bounds
45
+ const widthDiff = totalWidth - elementRect.width;
46
+ const heightDiff = totalHeight - elementRect.height;
47
+
48
+ return {
49
+ left: elementRect.left - widthDiff / 2,
50
+ right: elementRect.right + widthDiff / 2,
51
+ top: elementRect.top - heightDiff / 2,
52
+ bottom: elementRect.bottom + heightDiff / 2,
53
+ };
54
+ }
@@ -28,6 +28,13 @@ function isAndroid(): boolean {
28
28
  return /Android/.test(ua());
29
29
  }
30
30
 
31
+ function isMac(): boolean {
32
+ if (typeof navigator === 'undefined') {
33
+ return false;
34
+ }
35
+ return /^Mac/.test(navigator.platform ?? '') && !isIOS();
36
+ }
37
+
31
38
  function isJsdom(): boolean {
32
39
  return /jsdom/i.test(ua());
33
40
  }
@@ -40,6 +47,9 @@ export const platform = {
40
47
  get android() {
41
48
  return isAndroid();
42
49
  },
50
+ get mac() {
51
+ return isMac();
52
+ },
43
53
  },
44
54
  engine: {
45
55
  get webkit() {
@@ -2,3 +2,9 @@
2
2
  export * from './store';
3
3
  export * from './popupStoreUtils';
4
4
  export * from './popupTriggerMap';
5
+ export {
6
+ createInlineMiddleware,
7
+ getInlineRectTriggerProps,
8
+ updateInlineRectCoords,
9
+ type InlineRectCoords,
10
+ } from './inlineRect';
@@ -0,0 +1,289 @@
1
+ // Ported from .base-ui/packages/react/src/utils/popups/inlineRect.ts (v1.6.0). Floating UI ships an
2
+ // `inline()` middleware; this local version mirrors its line-rect selection while adding trigger
3
+ // identity checks, delayed-open hit-line reuse, and better left/right edge grouping for Preview
4
+ // Card's reusable trigger model. Pure geometry plus a floating-ui middleware — transcribed as-is,
5
+ // with React's ref types collapsed to plain `{ current }` objects and native event handlers.
6
+ import { isElement } from '../dom';
7
+
8
+ export interface InlineRectCoords {
9
+ /** The x position in viewport coordinates. */
10
+ x: number;
11
+ /** The y position in viewport coordinates. */
12
+ y: number;
13
+ /** The line index under the pointer when coordinates were captured. */
14
+ lineIndex?: number | undefined;
15
+ /** The trigger element whose rects produced these coordinates. */
16
+ element: Element;
17
+ }
18
+
19
+ interface RectLike {
20
+ left: number;
21
+ top: number;
22
+ right: number;
23
+ bottom: number;
24
+ width: number;
25
+ height: number;
26
+ }
27
+
28
+ interface ClientRectLike extends RectLike {
29
+ x: number;
30
+ y: number;
31
+ }
32
+
33
+ interface ClientRectsReference {
34
+ getClientRects(): ArrayLike<RectLike>;
35
+ }
36
+
37
+ function createRect(left: number, top: number, right: number, bottom: number): ClientRectLike {
38
+ return {
39
+ left,
40
+ top,
41
+ right,
42
+ bottom,
43
+ x: left,
44
+ y: top,
45
+ width: right - left,
46
+ height: bottom - top,
47
+ };
48
+ }
49
+
50
+ function copyRect(rect: RectLike): RectLike {
51
+ return {
52
+ left: rect.left,
53
+ top: rect.top,
54
+ right: rect.right,
55
+ bottom: rect.bottom,
56
+ width: rect.width,
57
+ height: rect.height,
58
+ };
59
+ }
60
+
61
+ function getLineRects(rects: ArrayLike<RectLike>) {
62
+ const lines: RectLike[] = [];
63
+ let previousRect: RectLike | undefined;
64
+ let left = Number.POSITIVE_INFINITY;
65
+ let top = Number.POSITIVE_INFINITY;
66
+ let right = Number.NEGATIVE_INFINITY;
67
+ let bottom = Number.NEGATIVE_INFINITY;
68
+
69
+ for (const rect of Array.from(rects).sort((a, b) => a.top - b.top)) {
70
+ left = Math.min(left, rect.left);
71
+ top = Math.min(top, rect.top);
72
+ right = Math.max(right, rect.right);
73
+ bottom = Math.max(bottom, rect.bottom);
74
+
75
+ if (!previousRect || rect.top - previousRect.top > previousRect.height / 2) {
76
+ lines.push(copyRect(rect));
77
+ } else {
78
+ const line = lines[lines.length - 1];
79
+ line.left = Math.min(line.left, rect.left);
80
+ line.right = Math.max(line.right, rect.right);
81
+ line.bottom = Math.max(line.bottom, rect.bottom);
82
+ line.width = line.right - line.left;
83
+ line.height = line.bottom - line.top;
84
+ }
85
+
86
+ previousRect = rect;
87
+ }
88
+
89
+ return {
90
+ lines,
91
+ fallback: createRect(left, top, right, bottom),
92
+ };
93
+ }
94
+
95
+ function findLineIndex(lines: RectLike[], x: number, y: number) {
96
+ return lines.findIndex(
97
+ (lineRect) =>
98
+ x > lineRect.left - 2 &&
99
+ x < lineRect.right + 2 &&
100
+ y > lineRect.top - 2 &&
101
+ y < lineRect.bottom + 2,
102
+ );
103
+ }
104
+
105
+ function createClientRect(rect: RectLike) {
106
+ return createRect(rect.left, rect.top, rect.right, rect.bottom);
107
+ }
108
+
109
+ function getInlineRectCoords(
110
+ element: Element,
111
+ clientX: number,
112
+ clientY: number,
113
+ ): InlineRectCoords | undefined {
114
+ const { lines } = getLineRects(element.getClientRects());
115
+
116
+ if (lines.length < 2) {
117
+ return undefined;
118
+ }
119
+
120
+ const lineIndex = findLineIndex(lines, clientX, clientY);
121
+
122
+ return {
123
+ x: clientX,
124
+ y: clientY,
125
+ lineIndex: lineIndex === -1 ? undefined : lineIndex,
126
+ element,
127
+ };
128
+ }
129
+
130
+ function getInlineReferenceRect(
131
+ reference: ClientRectsReference,
132
+ placement: string,
133
+ coords: InlineRectCoords | undefined,
134
+ ): ClientRectLike | null {
135
+ const { lines, fallback } = getLineRects(reference.getClientRects());
136
+
137
+ if (lines.length < 2) {
138
+ return null;
139
+ }
140
+
141
+ const x = coords?.x;
142
+ const y = coords?.y;
143
+ const side = placement[0];
144
+
145
+ if (coords?.lineIndex != null && lines[coords.lineIndex]) {
146
+ return createClientRect(lines[coords.lineIndex]);
147
+ }
148
+
149
+ if (x != null && y != null) {
150
+ const lineIndex = findLineIndex(lines, x, y);
151
+
152
+ if (lineIndex !== -1) {
153
+ return createClientRect(lines[lineIndex]);
154
+ }
155
+ }
156
+
157
+ if (lines.length === 2 && lines[0].left > lines[1].right && x != null && y != null) {
158
+ return fallback;
159
+ }
160
+
161
+ if (side === 't' || side === 'b') {
162
+ const firstRect = lines[0];
163
+ const lastRect = lines[lines.length - 1];
164
+ const targetRect = side === 't' ? firstRect : lastRect;
165
+
166
+ return createRect(targetRect.left, firstRect.top, targetRect.right, lastRect.bottom);
167
+ }
168
+
169
+ const isLeft = side === 'l';
170
+ let left = lines[0].left;
171
+ let right = lines[0].right;
172
+ let edge = isLeft ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
173
+ let targetFirstRect = lines[0];
174
+ let targetLastRect = lines[0];
175
+
176
+ for (const rect of lines) {
177
+ left = Math.min(left, rect.left);
178
+ right = Math.max(right, rect.right);
179
+
180
+ const nextEdge = isLeft ? rect.left : rect.right;
181
+
182
+ if ((isLeft && nextEdge < edge) || (!isLeft && nextEdge > edge)) {
183
+ edge = nextEdge;
184
+ targetFirstRect = rect;
185
+ targetLastRect = rect;
186
+ } else if (nextEdge === edge) {
187
+ targetLastRect = rect;
188
+ }
189
+ }
190
+
191
+ return createRect(left, targetFirstRect.top, right, targetLastRect.bottom);
192
+ }
193
+
194
+ function getContextElement(reference: any): Element | undefined {
195
+ if ('contextElement' in reference && reference.contextElement) {
196
+ return reference.contextElement;
197
+ }
198
+
199
+ return isElement(reference) ? (reference as Element) : undefined;
200
+ }
201
+
202
+ export function getInlineRectTriggerProps(
203
+ coordsRef: { current: InlineRectCoords | undefined },
204
+ isOpen: boolean,
205
+ ): Record<string, any> {
206
+ function updateCoords(event: MouseEvent) {
207
+ updateInlineRectCoords(coordsRef, event.currentTarget as Element, event.clientX, event.clientY);
208
+ }
209
+
210
+ function updateCoordsIfClosed(event: MouseEvent) {
211
+ if (!isOpen) {
212
+ updateCoords(event);
213
+ }
214
+ }
215
+
216
+ return {
217
+ onFocus() {
218
+ coordsRef.current = undefined;
219
+ },
220
+ onMouseEnter: updateCoordsIfClosed,
221
+ onMouseMove: updateCoordsIfClosed,
222
+ };
223
+ }
224
+
225
+ export function updateInlineRectCoords(
226
+ coordsRef: { current: InlineRectCoords | undefined },
227
+ element: Element,
228
+ clientX: number,
229
+ clientY: number,
230
+ ) {
231
+ const nextCoords = getInlineRectCoords(element, clientX, clientY);
232
+ coordsRef.current = nextCoords;
233
+ return nextCoords;
234
+ }
235
+
236
+ export function createInlineMiddleware(coordsRef: { current: InlineRectCoords | undefined }): any {
237
+ return {
238
+ name: 'inline',
239
+ async fn(state: any) {
240
+ const reference = state.elements.reference;
241
+
242
+ if (
243
+ typeof (reference as Partial<ClientRectsReference> | null)?.getClientRects !== 'function'
244
+ ) {
245
+ return {};
246
+ }
247
+
248
+ const contextElement = getContextElement(reference);
249
+ const coords = coordsRef.current;
250
+ const currentCoords =
251
+ coords?.element === reference || coords?.element === contextElement ? coords : undefined;
252
+ const rect = getInlineReferenceRect(
253
+ reference as ClientRectsReference,
254
+ state.placement,
255
+ currentCoords,
256
+ );
257
+
258
+ if (!rect || typeof state.platform.getElementRects !== 'function') {
259
+ return {};
260
+ }
261
+
262
+ const resetRects = await state.platform.getElementRects({
263
+ reference: {
264
+ contextElement,
265
+ getBoundingClientRect() {
266
+ return rect;
267
+ },
268
+ },
269
+ floating: state.elements.floating,
270
+ strategy: state.strategy,
271
+ });
272
+
273
+ if (
274
+ state.rects.reference.x === resetRects.reference.x &&
275
+ state.rects.reference.y === resetRects.reference.y &&
276
+ state.rects.reference.width === resetRects.reference.width &&
277
+ state.rects.reference.height === resetRects.reference.height
278
+ ) {
279
+ return {};
280
+ }
281
+
282
+ return {
283
+ reset: {
284
+ rects: resetRects,
285
+ },
286
+ };
287
+ },
288
+ };
289
+ }