Primitives / foldkit-primitives/interaction

Placing

namespace · packages/primitives/src/interaction/placing.ts:36

export * as Placing from './placing.js'

Keeps a floating panel inside the viewport: on insert it shifts left past the right edge and flips above its trigger when the bottom overflows and the room overhead fits; on release every inline prop goes. placeFor is pure geometry over rects, so the rule is unit-tested and the Mount only reads and writes. Inert without layout (zero rects never overflow), with no timers, observers, or reposition loops.

placeAt is the other half, for a popup that must open under the trigger that opened it. It writes left and --fk-placed-top once, relative to the popup's offset parent; the panel's stylesheet reads the variable as top, so keepWithin can still replace top when it flips. Key the popup by the trigger: a Mount reads its args at insert and never again. placeAtPoint is the same write for a viewport point, when the open event carried coordinates the message builder dropped. The two mounts start together, so the fit may run before that write; the write measures again. A point that would hang past the bottom shifts up. A trigger still flips, and the panel is marked data-fk-placed="above" so a hover bridge can follow the side that opened. Zero-size rects stay at the origin, so an inert document writes nothing a layout pass would have to undo.

Three related pieces this is not:

  • Anchor.behavior (foldkit-mixins-ui) binds a floating element to another element's id through floating-ui, with portals and continuous repositioning. It relocates nodes out from under the runtime (portal by default) and burns under zero geometry; reach for it when the panel must track a moving anchor, not for a popup placed once.
  • KeepInView (foldkit-primitives/dom) scrolls newly marked content into view. Same verb, opposite direction: that one moves the page to the panel, this one moves the panel into the page.
  • Overlay.behaviors owns dismissal, focus, scroll lock, and inertness, but no positioning; spread this alongside it for the full overlay.

In the guides

Members

Placing.AnchorRect

interface · packages/primitives/src/interaction/placing.ts:89

interface AnchorRect {
  readonly left: number
  readonly top: number
  readonly bottom: number
  readonly width: number
  readonly height: number
}

Placing.KeepWithin

const · packages/primitives/src/interaction/placing.ts:202

const KeepWithin: Mount.MountDefinitionNoArgs<'KeepWithin', never>

Placing.keepWithin

function · packages/primitives/src/interaction/placing.ts:329

function keepWithin<Slots>(
  slots: Slots,
): <Input, ParentMessage>(options: {
  readonly panel: keyof Slots & string
}) => Behavior.NamedBehavior<Slots, Input, ParentMessage>

Placing.OriginRect

interface · packages/primitives/src/interaction/placing.ts:97

interface OriginRect {
  readonly left: number
  readonly top: number
}

Placing.PanelRect

interface · packages/primitives/src/interaction/placing.ts:40

interface PanelRect {
  readonly top: number
  readonly right: number
  readonly bottom: number
  readonly height: number
}

Placing.PlaceAt

const · packages/primitives/src/interaction/placing.ts:236

const PlaceAt: Mount.MountDefinitionWithArgs<
  'PlaceAt',
  { triggerId: Schema.String; gap: Schema.Number },
  never
>

Placing.placeAt

function · packages/primitives/src/interaction/placing.ts:113

function placeAt(
  trigger: AnchorRect,
  origin: OriginRect,
  gap?: number,
): PlacedAt

Where a popup sits under its trigger's left edge, in the offset parent's coordinates. A zero-size trigger (an inert document) stays at the origin.

Placing.PlaceAtPoint

const · packages/primitives/src/interaction/placing.ts:277

const PlaceAtPoint: Mount.MountDefinitionWithArgs<
  'PlaceAtPoint',
  { x: Schema.Number; y: Schema.Number },
  never
>

Placing.placeAtPoint

function · packages/primitives/src/interaction/placing.ts:129

function placeAtPoint(point: ViewportPoint, origin: OriginRect): PlacedAt

Where a popup's top-left sits on a viewport point, in the offset parent's coordinates. A context menu uses it: the right-click event carries the point, and OnContextMenu does not.

Placing.placeAtTrigger

function · packages/primitives/src/interaction/placing.ts:303

function placeAtTrigger<Slots>(
  slots: Slots,
): <Input, ParentMessage>(options: {
  readonly panel: keyof Slots & string
  readonly triggerId: (input: Input) => string
  readonly gap?: number
  readonly at?: (input: Input) => Option.Option<ViewportPoint>
}) => Behavior.NamedBehavior<Slots, Input, ParentMessage>

Placing.Placed

interface · packages/primitives/src/interaction/placing.ts:52

interface Placed {
  readonly dx: number
  readonly flip: boolean
}

Placing.placedAbove

const · packages/primitives/src/interaction/placing.ts:87

const placedAbove: 'above'

Value of placedSide while the panel stands above its anchor.

Placing.PlacedAt

interface · packages/primitives/src/interaction/placing.ts:102

interface PlacedAt {
  readonly left: number
  readonly top: number
}

Placing.placedSide

const · packages/primitives/src/interaction/placing.ts:84

const placedSide: 'data-fk-placed'

Attribute keepWithin sets to placedAbove when the panel flips.

Placing.placedTop

const · packages/primitives/src/interaction/placing.ts:109

const placedTop: '--fk-placed-top'

Custom property PlaceAt writes. A panel reads it as top so a viewport flip, which sets the top property, still wins.

Placing.placeFor

function · packages/primitives/src/interaction/placing.ts:61

function placeFor(rect: PanelRect, viewport: Viewport, margin?: number): Placed

Where the panel goes so it fits: left by the overflow, above when the bottom clears the window and the room overhead fits the panel.

Placing.shiftIntoViewport

function · packages/primitives/src/interaction/placing.ts:71

function shiftIntoViewport(
  rect: {
    readonly top: number
    readonly bottom: number
  },
  viewportHeight: number,
  margin?: number,
): number

Pixels to move a box up so its bottom sits margin inside the viewport. 0 when it already fits, or when its top is already at the margin.

Placing.Viewport

interface · packages/primitives/src/interaction/placing.ts:47

interface Viewport {
  readonly width: number
  readonly height: number
}

Placing.ViewportPoint

interface · packages/primitives/src/interaction/placing.ts:121

interface ViewportPoint {
  readonly x: number
  readonly y: number
}