Skip to content

feat: update to upstream @floating-ui/core@1.8.0#363

Draft
rust-for-web[bot] wants to merge 1 commit into
mainfrom
upstream/core-1.8.0
Draft

feat: update to upstream @floating-ui/core@1.8.0#363
rust-for-web[bot] wants to merge 1 commit into
mainfrom
upstream/core-1.8.0

Conversation

@rust-for-web

@rust-for-web rust-for-web Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Release
@floating-ui/core@1.8.0

Diff for packages/core

Diff
diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md
index 533bbe94..85807f1e 100644
--- a/packages/core/CHANGELOG.md
+++ b/packages/core/CHANGELOG.md
@@ -1,5 +1,19 @@
 # @floating-ui/core
 
+## 1.8.0
+
+### Minor Changes
+
+- feat: add `'layoutViewport'` string option to `rootBoundary`. Unlike the visual `'viewport'` boundary, it remains stable while pinch-zooming or when a mobile software keyboard is open, and unlike a manually passed `Rect` of the documentElement's client size, it accounts for space reserved by `scrollbar-gutter: stable`.
+
+### Patch Changes
+
+- fix(inline): no-op on empty client rects and detect RTL disjoined line rects
+- fix: support explicit `undefined` for optional properties with `exactOptionalPropertyTypes`
+- fix(size): correctly compute available size for centered elements overflowing both sides
+- perf(core): reduce bundle size with behavior-identical middleware simplifications
+- Update dependencies: `@floating-ui/utils@0.2.12`
+
 ## 1.7.5
 
 ### Patch Changes
diff --git a/packages/core/package.json b/packages/core/package.json
index 3d74b6d2..40211486 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@floating-ui/core",
-  "version": "1.7.5",
+  "version": "1.8.0",
   "description": "Positioning library for floating elements: tooltips, popovers, dropdowns, and more",
   "publishConfig": {
     "access": "public"
@@ -29,7 +29,7 @@
     "test": "vitest run",
     "test:watch": "vitest watch",
     "lint": "eslint .",
-    "format": "prettier --write .",
+    "format": "prettier --ignore-path ../../.gitignore --write .",
     "clean": "rimraf dist out-tsc",
     "dev": "rollup -c -w",
     "build": "rollup -c",
diff --git a/packages/core/src/computeCoordsFromPlacement.ts b/packages/core/src/computeCoordsFromPlacement.ts
index f84c33f6..e5263859 100644
--- a/packages/core/src/computeCoordsFromPlacement.ts
+++ b/packages/core/src/computeCoordsFromPlacement.ts
@@ -40,14 +40,12 @@ export function computeCoordsFromPlacement(
       coords = {x: reference.x, y: reference.y};
   }
 
-  switch (getAlignment(placement)) {
-    case 'start':
-      coords[alignmentAxis] -= commonAlign * (rtl && isVertical ? -1 : 1);
-      break;
-    case 'end':
-      coords[alignmentAxis] += commonAlign * (rtl && isVertical ? -1 : 1);
-      break;
-    default:
+  const alignment = getAlignment(placement);
+  if (alignment) {
+    coords[alignmentAxis] +=
+      commonAlign *
+      (alignment === 'end' ? 1 : -1) *
+      (rtl && isVertical ? -1 : 1);
   }
 
   return coords;
diff --git a/packages/core/src/detectOverflow.ts b/packages/core/src/detectOverflow.ts
index 6d005e9b..a1db303a 100644
--- a/packages/core/src/detectOverflow.ts
+++ b/packages/core/src/detectOverflow.ts
@@ -14,28 +14,28 @@ export interface DetectOverflowOptions {
    * The clipping element(s) or area in which overflow will be checked.
    * @default 'clippingAncestors'
    */
-  boundary?: Boundary;
+  boundary?: Boundary | undefined;
   /**
    * The root clipping area in which overflow will be checked.
    * @default 'viewport'
    */
-  rootBoundary?: RootBoundary;
+  rootBoundary?: RootBoundary | undefined;
   /**
    * The element in which overflow is being checked relative to a boundary.
    * @default 'floating'
    */
-  elementContext?: ElementContext;
+  elementContext?: ElementContext | undefined;
   /**
    * Whether to check for overflow using the alternate element's boundary
    * (`clippingAncestors` boundary only).
    * @default false
    */
-  altBoundary?: boolean;
+  altBoundary?: boolean | undefined;
   /**
    * Virtual padding for the resolved overflow detection offsets.
    * @default 0
    */
-  padding?: Padding;
+  padding?: Padding | undefined;
 }
 
 /**
@@ -83,9 +83,8 @@ export async function detectOverflow(
       : rects.reference;
 
   const offsetParent = await platform.getOffsetParent?.(elements.floating);
-  const offsetScale = (await platform.isElement?.(offsetParent))
-    ? (await platform.getScale?.(offsetParent)) || {x: 1, y: 1}
-    : {x: 1, y: 1};
+  const offsetScale = ((await platform.isElement?.(offsetParent)) &&
+    (await platform.getScale?.(offsetParent))) || {x: 1, y: 1};
 
   const elementClientRect = rectToClientRect(
     platform.convertOffsetParentRelativeRectToViewportRelativeRect
diff --git a/packages/core/src/middleware/arrow.ts b/packages/core/src/middleware/arrow.ts
index 62a7ba7d..cf40a727 100644
--- a/packages/core/src/middleware/arrow.ts
+++ b/packages/core/src/middleware/arrow.ts
@@ -6,7 +6,7 @@ import {
   getAlignmentAxis,
   getAxisLength,
   getPaddingObject,
-  min as mathMin,
+  min,
 } from '@floating-ui/utils';
 
 import type {Derivable, Middleware} from '../types';
@@ -22,7 +22,7 @@ export interface ArrowOptions {
    * Useful when the floating element has rounded corners.
    * @default 0
    */
-  padding?: Padding;
+  padding?: Padding | undefined;
 }
 
 /**
@@ -75,16 +75,15 @@ export const arrow = (
     // centered, modify the padding so that it is centered.
     const largestPossiblePadding =
       clientSize / 2 - arrowDimensions[length] / 2 - 1;
-    const minPadding = mathMin(paddingObject[minProp], largestPossiblePadding);
-    const maxPadding = mathMin(paddingObject[maxProp], largestPossiblePadding);
+    const minPadding = min(paddingObject[minProp], largestPossiblePadding);
+    const maxPadding = min(paddingObject[maxProp], largestPossiblePadding);
 
     // Make sure the arrow doesn't overflow the floating element if the center
     // point is outside the floating element's bounds.
-    const min = minPadding;
     const max = clientSize - arrowDimensions[length] - maxPadding;
     const center =
       clientSize / 2 - arrowDimensions[length] / 2 + centerToReference;
-    const offset = clamp(min, center, max);
+    const offset = clamp(minPadding, center, max);
 
     // If the reference is small enough that the arrow's padding causes it to
     // to point to nothing for an aligned placement, adjust the offset of the
@@ -95,12 +94,12 @@ export const arrow = (
       getAlignment(placement) != null &&
       center !== offset &&
       rects.reference[length] / 2 -
-        (center < min ? minPadding : maxPadding) -
+        (center < minPadding ? minPadding : maxPadding) -
         arrowDimensions[length] / 2 <
         0;
     const alignmentOffset = shouldAddOffset
-      ? center < min
-        ? center - min
+      ? center < minPadding
+        ? center - minPadding
         : center - max
       : 0;
 
diff --git a/packages/core/src/middleware/autoPlacement.ts b/packages/core/src/middleware/autoPlacement.ts
index e769967c..a36a6ec0 100644
--- a/packages/core/src/middleware/autoPlacement.ts
+++ b/packages/core/src/middleware/autoPlacement.ts
@@ -47,24 +47,24 @@ export interface AutoPlacementOptions extends DetectOverflowOptions {
    * whether to check for most space along this axis.
    * @default false
    */
-  crossAxis?: boolean;
+  crossAxis?: boolean | undefined;
   /**
    * Choose placements with a particular alignment.
    * @default undefined
    */
-  alignment?: Alignment | null;
+  alignment?: Alignment | null | undefined;
   /**
    * Whether to choose placements with the opposite alignment if the preferred
    * alignment does not fit.
    * @default true
    */
-  autoAlignment?: boolean;
+  autoAlignment?: boolean | undefined;
   /**
    * Which placements are allowed to be chosen. Placements must be within the
    * `alignment` option if explicitly set.
    * @default allPlacements (variable)
    */
-  allowedPlacements?: Array<Placement>;
+  allowedPlacements?: Array<Placement> | undefined;
 }
 
 /**
@@ -94,11 +94,6 @@ export const autoPlacement = (
         ? getPlacementList(alignment || null, autoAlignment, allowedPlacements)
         : allowedPlacements;
 
-    const overflow = await platform.detectOverflow(
-      state,
-      detectOverflowOptions,
-    );
-
     const currentIndex = middlewareData.autoPlacement?.index || 0;
     const currentPlacement = placements[currentIndex];
 
@@ -106,12 +101,6 @@ export const autoPlacement = (
       return {};
     }
 
-    const alignmentSides = getAlignmentSides(
-      currentPlacement,
-      rects,
-      await platform.isRTL?.(elements.floating),
-    );
-
     // Make `computeCoords` start from the right place.
     if (placement !== currentPlacement) {
       return {
@@ -121,6 +110,17 @@ export const autoPlacement = (
       };
     }
 
+    const overflow = await platform.detectOverflow(
+      state,
+      detectOverflowOptions,
+    );
+
+    const alignmentSides = getAlignmentSides(
+      currentPlacement,
+      rects,
+      await platform.isRTL?.(elements.floating),
+    );
+
     const currentOverflows = [
       overflow[getSide(currentPlacement)],
       overflow[alignmentSides[0]],
diff --git a/packages/core/src/middleware/flip.ts b/packages/core/src/middleware/flip.ts
index 8f87f499..0605a4ed 100644
--- a/packages/core/src/middleware/flip.ts
+++ b/packages/core/src/middleware/flip.ts
@@ -18,7 +18,7 @@ export interface FlipOptions extends DetectOverflowOptions {
    * whether overflow along this axis is checked to perform a flip.
    * @default true
    */
-  mainAxis?: boolean;
+  mainAxis?: boolean | undefined;
   /**
    * The axis that runs along the alignment of the floating element. Determines
    * whether overflow along this axis is checked to perform a flip.
@@ -27,29 +27,29 @@ export interface FlipOptions extends DetectOverflowOptions {
    * - `'alignment'`: Whether to check cross axis overflow for alignment flipping only.
    * @default true
    */
-  crossAxis?: boolean | 'alignment';
+  crossAxis?: boolean | 'alignment' | undefined;
   /**
    * Placements to try sequentially if the preferred `placement` does not fit.
    * @default [oppositePlacement] (computed)
    */
-  fallbackPlacements?: Array<Placement>;
+  fallbackPlacements?: Array<Placement> | undefined;
   /**
    * What strategy to use when no placements fit.
    * @default 'bestFit'
    */
-  fallbackStrategy?: 'bestFit' | 'initialPlacement';
+  fallbackStrategy?: 'bestFit' | 'initialPlacement' | undefined;
   /**
    * Whether to allow fallback to the perpendicular axis of the preferred
    * placement, and if so, which side direction along the axis to prefer.
    * @default 'none' (disallow fallback)
    */
-  fallbackAxisSideDirection?: 'none' | 'start' | 'end';
+  fallbackAxisSideDirection?: 'none' | 'start' | 'end' | undefined;
   /**
    * Whether to flip to placements with the opposite alignment if they fit
    * better.
    * @default true
    */
-  flipAlignment?: boolean;
+  flipAlignment?: boolean | undefined;
 }
 
 /**
diff --git a/packages/core/src/middleware/hide.ts b/packages/core/src/middleware/hide.ts
index 914db8be..e3bc76a6 100644
--- a/packages/core/src/middleware/hide.ts
+++ b/packages/core/src/middleware/hide.ts
@@ -21,7 +21,7 @@ export interface HideOptions extends DetectOverflowOptions {
   /**
    * The strategy used to determine when to hide the floating element.
    */
-  strategy?: 'referenceHidden' | 'escaped';
+  strategy?: 'referenceHidden' | 'escaped' | undefined;
 }
 
 /**
diff --git a/packages/core/src/middleware/inline.ts b/packages/core/src/middleware/inline.ts
index ecc75a35..364de2f4 100644
--- a/packages/core/src/middleware/inline.ts
+++ b/packages/core/src/middleware/inline.ts
@@ -45,17 +45,17 @@ export interface InlineOptions {
    * Viewport-relative `x` coordinate to choose a `ClientRect`.
    * @default undefined
    */
-  x?: number;
+  x?: number | undefined;
   /**
    * Viewport-relative `y` coordinate to choose a `ClientRect`.
    * @default undefined
    */
-  y?: number;
+  y?: number | undefined;
   /**
    * Represents the padding around a disjoined rect when choosing it.
    * @default 2
    */
-  padding?: Padding;
+  padding?: Padding | undefined;
 }
 
 /**
@@ -79,6 +79,13 @@ export const inline = (
       (await platform.getClientRects?.(elements.reference)) || [],
     );
 
+    // No rects (e.g. a hidden or detached reference, or a collapsed range) —
+    // keep the existing reference rect rather than resetting to an invalid
+    // one with non-finite values.
+    if (!nativeClientRects.length) {
+      return {};
+    }
+
     const clientRects = getRectsByLine(nativeClientRects);
     const fallback = rectToClientRect(getBoundingRect(nativeClientRects));
     const paddingObject = getPaddingObject(padding);
@@ -87,7 +94,8 @@ export const inline = (
       // There are two rects and they are disjoined.
       if (
         clientRects.length === 2 &&
-        clientRects[0].left > clientRects[1].right &&
+        (clientRects[0].left > clientRects[1].right ||
+          clientRects[1].left > clientRects[0].right) &&
         x != null &&
         y != null
       ) {
@@ -114,19 +122,13 @@ export const inline = (
           const bottom = lastRect.bottom;
           const left = isTop ? firstRect.left : lastRect.left;
           const right = isTop ? firstRect.right : lastRect.right;
-          const width = right - left;
-          const height = bottom - top;
-
-          return {
-            top,
-            bottom,
-            left,
-            right,
-            width,
-            height,
+
+          return rectToClientRect({
             x: left,
             y: top,
-          };
+            width: right - left,
+            height: bottom - top,
+          });
         }
 
         const isLeftSide = getSide(placement) === 'left';
@@ -138,21 +140,13 @@ export const inline = (
 
         const top = measureRects[0].top;
         const bottom = measureRects[measureRects.length - 1].bottom;
-        const left = minLeft;
-        const right = maxRight;
-        const width = right - left;
-        const height = bottom - top;
-
-        return {
-          top,
-          bottom,
-          left,
-          right,
-          width,
-          height,
-          x: left,
+
+        return rectToClientRect({
+          x: minLeft,
           y: top,
-        };
+          width: maxRight - minLeft,
+          height: bottom - top,
+        });
       }
 
       return fallback;
diff --git a/packages/core/src/middleware/offset.ts b/packages/core/src/middleware/offset.ts
index cdb45ce1..158c6902 100644
--- a/packages/core/src/middleware/offset.ts
+++ b/packages/core/src/middleware/offset.ts
@@ -18,13 +18,13 @@ type OffsetValue =
        * element.
        * @default 0
        */
-      mainAxis?: number;
+      mainAxis?: number | undefined;
       /**
        * The axis that runs along the alignment of the floating element.
        * Represents the skidding between the reference and floating element.
        * @default 0
        */
-      crossAxis?: number;
+      crossAxis?: number | undefined;
       /**
        * The same axis as `crossAxis` but applies only to aligned placements
        * and inverts the `end` alignment. When set to a number, it overrides the
@@ -35,14 +35,14 @@ type OffsetValue =
        * the reverse.
        * @default null
        */
-      alignmentAxis?: number | null;
+      alignmentAxis?: number | null | undefined;
     };
 
 // For type backwards-compatibility, the `OffsetOptions` type was also
 // Derivable.
 export type OffsetOptions = OffsetValue | Derivable<OffsetValue>;
 
-export async function convertValueToCoords(
+async function convertValueToCoords(
   state: MiddlewareState,
   options: OffsetOptions,
 ): Promise<Coords> {
diff --git a/packages/core/src/middleware/shift.ts b/packages/core/src/middleware/shift.ts
index 7c9ca94a..15c7c689 100644
--- a/packages/core/src/middleware/shift.ts
+++ b/packages/core/src/middleware/shift.ts
@@ -17,21 +17,23 @@ export interface ShiftOptions extends DetectOverflowOptions {
    * whether overflow along this axis is checked to perform shifting.
    * @default true
    */
-  mainAxis?: boolean;
+  mainAxis?: boolean | undefined;
   /**
    * The axis that runs along the side of the floating element. Determines
    * whether overflow along this axis is checked to perform shifting.
    * @default false
    */
-  crossAxis?: boolean;
+  crossAxis?: boolean | undefined;
   /**
    * Accepts a function that limits the shifting done in order to prevent
    * detachment.
    */
-  limiter?: {
-    fn: (state: MiddlewareState) => Coords;
-    options?: any;
-  };
+  limiter?:
+    | {
+        fn: (state: MiddlewareState) => Coords;
+        options?: any;
+      }
+    | undefined;
 }
 
 /**
@@ -59,28 +61,25 @@ export const shift = (
       state,
       detectOverflowOptions,
     );
-    const crossAxis = getSideAxis(getSide(placement));
+    const crossAxis = getSideAxis(placement);
     const mainAxis = getOppositeAxis(crossAxis);
 
     let mainAxisCoord = coords[mainAxis];
     let crossAxisCoord = coords[crossAxis];
 
-    if (checkMainAxis) {
-      const minSide = mainAxis === 'y' ? 'top' : 'left';
-      const maxSide = mainAxis === 'y' ? 'bottom' : 'right';
-      const min = mainAxisCoord + overflow[minSide];
-      const max = mainAxisCoord - overflow[maxSide];
+    const clampCoord = (axis: 'x' | 'y', coord: number) =>
+      clamp(
+        coord + overflow[axis === 'y' ? 'top' : 'left'],
+        coord,
+        coord - overflow[axis === 'y' ? 'bottom' : 'right'],
+      );
 
-      mainAxisCoord = clamp(min, mainAxisCoord, max);
+    if (checkMainAxis) {
+      mainAxisCoord = clampCoord(mainAxis, mainAxisCoord);
     }
 
     if (checkCrossAxis) {
-      const minSide = crossAxis === 'y' ? 'top' : 'left';
-      const maxSide = crossAxis === 'y' ? 'bottom' : 'right';
-      const min = crossAxisCoord + overflow[minSide];
-      const max = crossAxisCoord - overflow[maxSide];
-
-      crossAxisCoord = clamp(min, crossAxisCoord, max);
+      crossAxisCoord = clampCoord(crossAxis, crossAxisCoord);
     }
 
     const limitedCoords = limiter.fn({
@@ -110,12 +109,12 @@ type LimitShiftOffset =
        * Offset the limiting of the axis that runs along the alignment of the
        * floating element.
        */
-      mainAxis?: number;
+      mainAxis?: number | undefined;
       /**
        * Offset the limiting of the axis that runs along the side of the
        * floating element.
        */
-      crossAxis?: number;
+      crossAxis?: number | undefined;
     };
 
 export interface LimitShiftOptions {
@@ -125,16 +124,16 @@ export interface LimitShiftOptions {
    * - positive = start limiting earlier
    * - negative = start limiting later
    */
-  offset?: LimitShiftOffset | Derivable<LimitShiftOffset>;
+  offset?: LimitShiftOffset | Derivable<LimitShiftOffset> | undefined;
   /**
    * Whether to limit the axis that runs along the alignment of the floating
    * element.
    */
-  mainAxis?: boolean;
+  mainAxis?: boolean | undefined;
   /**
    * Whether to limit the axis that runs along the side of the floating element.
    */
-  crossAxis?: boolean;
+  crossAxis?: boolean | undefined;
 }
 
 /**
@@ -167,7 +166,10 @@ export const limitShift = (
     const computedOffset =
       typeof rawOffset === 'number'
         ? {mainAxis: rawOffset, crossAxis: 0}
-        : {mainAxis: 0, crossAxis: 0, ...rawOffset};
+        : {
+            mainAxis: rawOffset.mainAxis ?? 0,
+            crossAxis: rawOffset.crossAxis ?? 0,
+          };
 
     if (checkMainAxis) {
       const len = mainAxis === 'y' ? 'height' : 'width';
diff --git a/packages/core/src/middleware/size.ts b/packages/core/src/middleware/size.ts
index 2d25b6f2..27d01e85 100644
--- a/packages/core/src/middleware/size.ts
+++ b/packages/core/src/middleware/size.ts
@@ -10,18 +10,31 @@ import {
 import type {DetectOverflowOptions} from '../detectOverflow';
 import type {Derivable, Middleware, MiddlewareState} from '../types';
 
+// Method syntax keeps callback parameters bivariant, but expressing the
+// explicit `| undefined` required by `exactOptionalPropertyTypes` needs
+// property syntax, which is contravariant under `strictFunctionTypes`.
+// Extracting the function from a method position restores that bivariance so
+// consumers can still assign callbacks with narrower parameter types.
+type BivariantCallback<T extends (...args: any[]) => any> = {
+  bivariance(...args: Parameters<T>): ReturnType<T>;
+}['bivariance'];
+
 export interface SizeOptions extends DetectOverflowOptions {
   /**
    * Function that is called to perform style mutations to the floating element
    * to change its size.
    * @default undefined
    */
-  apply?(
-    args: MiddlewareState & {
-      availableWidth: number;
-      availableHeight: number;
-    },
-  ): void | Promise<void>;
+  apply?:
+    | BivariantCallback<
+        (
+          args: MiddlewareState & {
+            availableWidth: number;
+            availableHeight: number;
+          },
+        ) => void | Promise<void>
+      >
+    | undefined;
 }
 
 /**
@@ -79,38 +92,24 @@ export const size = (
       maximumClippingWidth,
     );
 
-    const noShift = !state.middlewareData.shift;
+    const shiftData = state.middlewareData.shift;
+    const noShift = !shiftData;
 
     let availableHeight = overflowAvailableHeight;
     let availableWidth = overflowAvailableWidth;
 
-    if (state.middlewareData.shift?.enabled.x) {
+    if (shiftData?.enabled.x) {
       availableWidth = maximumClippingWidth;
     }
-    if (state.middlewareData.shift?.enabled.y) {
+    if (shiftData?.enabled.y) {
       availableHeight = maximumClippingHeight;
     }
 
     if (noShift && !alignment) {
-      const xMin = max(overflow.left, 0);
-      const xMax = max(overflow.right, 0);
-      const yMin = max(overflow.top, 0);
-      const yMax = max(overflow.bottom, 0);
-
       if (isYAxis) {
-        availableWidth =
-          width -
-          2 *
-            (xMin !== 0 || xMax !== 0
-              ? xMin + xMax
-              : max(overflow.left, overflow.right));
+        availableWidth = width - 2 * max(overflow.left, overflow.right);
       } else {
-        availableHeight =
-          height -
-          2 *
-            (yMin !== 0 || yMax !== 0
-              ? yMin + yMax
-              : max(overflow.top, overflow.bottom));
+        availableHeight = height - 2 * max(overflow.top, overflow.bottom);
       }
     }
 
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index c7402c13..ec9e8df7 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -38,51 +38,65 @@ export interface Platform {
   getDimensions: (element: any) => Promisable<Dimensions>;
 
   // Optional
-  convertOffsetParentRelativeRectToViewportRelativeRect?: (args: {
-    elements?: Elements;
-    rect: Rect;
-    offsetParent: any;
-    strategy: Strategy;
-  }) => Promisable<Rect>;
-  getOffsetParent?: (element: any) => Promisable<any>;
-  isElement?: (value: any) => Promisable<boolean>;
-  getDocumentElement?: (element: any) => Promisable<any>;
-  getClientRects?: (element: any) => Promisable<Array<ClientRectObject>>;
-  isRTL?: (element: any) => Promisable<boolean>;
-  getScale?: (element: any) => Promisable<{x: number; y: number}>;
-  detectOverflow?: typeof detectOverflow;
+  convertOffsetParentRelativeRectToViewportRelativeRect?:
+    | ((args: {
+        elements?: Elements | undefined;
+        rect: Rect;
+        offsetParent: any;
+        strategy: Strategy;
+      }) => Promisable<Rect>)
+    | undefined;
+  getOffsetParent?: ((element: any) => Promisable<any>) | undefined;
+  isElement?: ((value: any) => Promisable<boolean>) | undefined;
+  getDocumentElement?: ((element: any) => Promisable<any>) | undefined;
+  getClientRects?:
+    | ((element: any) => Promisable<Array<ClientRectObject>>)
+    | undefined;
+  isRTL?: ((element: any) => Promisable<boolean>) | undefined;
+  getScale?: ((element: any) => Promisable<{x: number; y: number}>) | undefined;
+  detectOverflow?: typeof detectOverflow | undefined;
 }
 
 export interface MiddlewareData {
   [key: string]: any;
-  arrow?: Partial<Coords> & {
-    centerOffset: number;
-    alignmentOffset?: number;
-  };
-  autoPlacement?: {
-    index?: number;
-    overflows: Array<{
-      placement: Placement;
-      overflows: Array<number>;
-    }>;
-  };
-  flip?: {
-    index?: number;
-    overflows: Array<{
-      placement: Placement;
-      overflows: Array<number>;
-    }>;
-  };
-  hide?: {
-    referenceHidden?: boolean;
-    escaped?: boolean;
-    referenceHiddenOffsets?: SideObject;
-    escapedOffsets?: SideObject;
-  };
-  offset?: Coords & {placement: Placement};
-  shift?: Coords & {
-    enabled: {[key in Axis]: boolean};
-  };
+  arrow?:
+    | (Partial<Coords> & {
+        centerOffset: number;
+        alignmentOffset?: number | undefined;
+      })
+    | undefined;
+  autoPlacement?:
+    | {
+        index?: number | undefined;
+        overflows: Array<{
+          placement: Placement;
+          overflows: Array<number>;
+        }>;
+      }
+    | undefined;
+  flip?:
+    | {
+        index?: number | undefined;
+        overflows: Array<{
+          placement: Placement;
+          overflows: Array<number>;
+        }>;
+      }
+    | undefined;
+  hide?:
+    | {
+        referenceHidden?: boolean | undefined;
+        escaped?: boolean | undefined;
+        referenceHiddenOffsets?: SideObject | undefined;
+        escapedOffsets?: SideObject | undefined;
+      }
+    | undefined;
+  offset?: (Coords & {placement: Placement}) | undefined;
+  shift?:
+    | (Coords & {
+        enabled: {[key in Axis]: boolean};
+      })
+    | undefined;
 }
 
 export interface ComputePositionConfig {
@@ -93,16 +107,16 @@ export interface ComputePositionConfig {
   /**
    * Where to place the floating element relative to the reference element.
    */
-  placement?: Placement;
+  placement?: Placement | undefined;
   /**
    * The strategy to use when positioning the floating element.
    */
-  strategy?: Strategy;
+  strategy?: Strategy | undefined;
   /**
    * Array of middleware objects to modify the positioning or provide data for
    * rendering.
    */
-  middleware?: Array<Middleware | null | undefined | false>;
+  middleware?: Array<Middleware | null | undefined | false> | undefined;
 }
 
 export interface ComputePositionReturn extends Coords {
@@ -127,15 +141,18 @@ export type ComputePosition = (
 ) => Promise<ComputePositionReturn>;
 
 export interface MiddlewareReturn extends Partial<Coords> {
-  data?: {
-    [key: string]: any;
-  };
+  data?:
+    | {
+        [key: string]: any;
+      }
+    | undefined;
   reset?:
     | boolean
     | {
-        placement?: Placement;
-        rects?: boolean | ElementRects;
-      };
+        placement?: Placement | undefined;
+        rects?: boolean | ElementRects | undefined;
+      }
+    | undefined;
 }
 
 export type Middleware = {
@@ -167,5 +184,5 @@ export interface MiddlewareState extends Coords {
 export type MiddlewareArguments = MiddlewareState;
 
 export type Boundary = any;
-export type RootBoundary = 'viewport' | 'document' | Rect;
+export type RootBoundary = 'viewport' | 'layoutViewport' | 'document' | Rect;
 export type ElementContext = 'reference' | 'floating';
diff --git a/packages/core/test/index.test-d.ts b/packages/core/test/index.test-d.ts
new file mode 100644
index 00000000..6ceac0c8
--- /dev/null
+++ b/packages/core/test/index.test-d.ts
@@ -0,0 +1,18 @@
+import type {MiddlewareState} from '../src';
+import {size} from '../src';
+
+// A callback with a narrower parameter type stays assignable to `apply`,
+// matching the behavior before optional callbacks gained an explicit
+// `| undefined` for `exactOptionalPropertyTypes`. Regresses to a TS2322 error
+// if `apply` loses its bivariant callback treatment.
+const narrowSizeApply = (
+  args: MiddlewareState & {
+    availableWidth: number;
+    availableHeight: number;
+    custom: true;
+  },
+) => {
+  args.custom;
+};
+
+size({apply: narrowSizeApply});
diff --git a/packages/core/test/middleware/inline.test.ts b/packages/core/test/middleware/inline.test.ts
index d47a82cb..062f7274 100644
--- a/packages/core/test/middleware/inline.test.ts
+++ b/packages/core/test/middleware/inline.test.ts
@@ -1,5 +1,8 @@
-import {rectToClientRect} from '../../src';
+import type {ClientRectObject} from '@floating-ui/utils';
+
+import {computePosition, inline, rectToClientRect} from '../../src';
 import {getRectsByLine} from '../../src/middleware/inline';
+import type {Platform} from '../../src/types';
 
 describe('getRectsByLine', () => {
   test('single line', () => {
@@ -50,3 +53,81 @@ describe('getRectsByLine', () => {
     ]);
   });
 });
+
+describe('inline', () => {
+  function createPlatform(clientRects: Array<ClientRectObject>): Platform {
+    return {
+      getElementRects: ({reference}: any) => {
+        const rect = reference.getBoundingClientRect?.() ?? {
+          x: 0,
+          y: 0,
+          width: 100,
+          height: 100,
+        };
+        return {
+          reference: {
+            x: rect.x,
+            y: rect.y,
+            width: rect.width,
+            height: rect.height,
+          },
+          floating: {x: 0, y: 0, width: 50, height: 50},
+        };
+      },
+      getDimensions: () => ({width: 50, height: 50}),
+      getClientRects: () => clientRects,
+    } as unknown as Platform;
+  }
+
+  test('no-ops when there are no client rects', async () => {
+    const {x, y} = await computePosition(
+      {},
+      {},
+      {
+        platform: createPlatform([]),
+        middleware: [inline()],
+      },
+    );
+
+    // The original reference rect is kept instead of resetting to one with
+    // non-finite values.
+    expect(x).toBe(25);
+    expect(y).toBe(100);
+  });
+
+  test('chooses the disjoined rect containing the point (LTR)', async () => {
+    // Top fragment sits to the right of the bottom fragment.
+    const {x, y} = await computePosition(
+      {},
+      {},
+      {
+        platform: createPlatform([
+          rectToClientRect({x: 200, y: 0, width: 100, height: 20}),
+          rectToClientRect({x: 0, y: 20, width: 100, height: 20}),
+        ]),
+        middleware: [inline({x: 250, y: 10})],
+      },
+    );
+
+    expect(x).toBe(225);
+    expect(y).toBe(20);
+  });
+
+  test('chooses the disjoined rect containing the point (RTL)', async () => {
+    // Top fragment sits to the left of the bottom fragment.
+    const {x, y} = await computePosition(
+      {},
+      {},
+      {
+        platform: createPlatform([
+          rectToClientRect({x: 0, y: 0, width: 100, height: 20}),
+          rectToClientRect({x: 200, y: 20, width: 100, height: 20}),
+        ]),
+        middleware: [inline({x: 50, y: 10})],
+      },
+    );
+
+    expect(x).toBe(25);
+    expect(y).toBe(20);
+  });
+});
diff --git a/packages/core/tsconfig.lib.json b/packages/core/tsconfig.lib.json
index ebd0c733..dc1afa72 100644
--- a/packages/core/tsconfig.lib.json
+++ b/packages/core/tsconfig.lib.json
@@ -1,7 +1,9 @@
 {
   "extends": "config/tsconfig.base.json",
   "compilerOptions": {
-    "outDir": "out-tsc"
+    "outDir": "out-tsc",
+    // TODO: move to config/tsconfig.base.json after https://github.com/vitejs/vite/issues/21715 is resolved.
+    "exactOptionalPropertyTypes": true
   },
   "include": ["src"],
   "references": [{"path": "../utils/tsconfig.lib.json"}]

Full diff
1.7.5...1.8.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants