ref() Single

ref() Single demo built with Expressive MVC - the complete source below runs as an editable sandbox when JavaScript is enabled.

App.tsx

import './App.css';

import { Component, ref } from '@expressive/react';
import type { PointerEvent } from 'react';

// `ref` is the useRef replacement: a slot for a value outside the render
// data - here the DOM node being dragged. Its callable form captures the
// node; `.current` reaches it to measure geometry. Position is plain x/y
// state, so the box just follows the numbers.
class Draggable extends Component {
  box = ref<HTMLDivElement>();

  x = 72;
  y = 64;
  dragging = false;

  offset = { x: 0, y: 0 };

  // Pointer events unify mouse and touch; capturing the pointer keeps the
  // drag tracking even when it outruns the box.
  grab(e: PointerEvent) {
    const rect = this.box.current!.getBoundingClientRect();

    this.offset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
    this.dragging = true;
    e.currentTarget.setPointerCapture(e.pointerId);
  }

  move(e: PointerEvent) {
    if (!this.dragging) return;

    const box = this.box.current!;
    const surface = box.parentElement!.getBoundingClientRect();

    this.x = clamp(e.clientX - surface.left - this.offset.x, 0, surface.width - box.offsetWidth);
    this.y = clamp(e.clientY - surface.top - this.offset.y, 0, surface.height - box.offsetHeight);
  }

  drop() {
    this.dragging = false;
  }

  render() {
    const { x, y, dragging } = this;

    return (
      <div className="container drag">
        <h1>Refs</h1>
        <p>
          Drag the box. Its position is plain <code>x</code>/<code>y</code>{' '}
          state; <code>ref</code> holds the node so the handler can measure it.
        </p>

        <div className="surface">
          <div
            ref={this.box}
            className={dragging ? 'box dragging' : 'box'}
            style={{ transform: `translate(${x}px, ${y}px)` }}
            onPointerDown={this.grab}
            onPointerMove={this.move}
            onPointerUp={this.drop}>
            {Math.round(x)}, {Math.round(y)}
          </div>
        </div>
      </div>
    );
  }
}

function clamp(n: number, min: number, max: number) {
  return Math.min(max, Math.max(min, n));
}

export default () => <Draggable />;

App.css

.drag {
  max-width: 30rem;
}

.surface {
  position: relative;
  width: 100%;
  height: 17rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
  -webkit-user-select: none;
  user-select: none;
  background-image: radial-gradient(var(--border) 1px, transparent 1px);
  background-size: 1.25rem 1.25rem;
}

.box {
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 5rem;
  height: 5rem;
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  color: var(--accent-fg);
  background: var(--accent);
  border-radius: var(--r);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.14);
  cursor: grab;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  transition: box-shadow 0.15s;
}

.box.dragging {
  cursor: grabbing;
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28);
}