or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

animation-and-transitions.mdevent-management.mdfocus-and-accessibility.mdid-and-refs.mdindex.mdlinks-and-navigation.mdmiscellaneous-utilities.mdplatform-detection.mdprops-and-events.mdscrolling-and-layout.mdshadow-dom-support.mdstate-and-effects.mdvirtual-events-and-input.md

miscellaneous-utilities.mddocs/

0

# Miscellaneous Utilities

1

2

Additional utilities for DOM helpers, value management, constants, and utility functions that support React Aria components.

3

4

## Capabilities

5

6

### DOM Helpers

7

8

Utilities for working with DOM elements and their properties.

9

10

```typescript { .api }

11

/**

12

* Gets element's offset position

13

* @param element - Target element

14

* @param reverse - Whether to measure from right/bottom (default: false)

15

* @param orientation - Measurement direction (default: 'horizontal')

16

* @returns Offset value in pixels

17

*/

18

function getOffset(

19

element: Element,

20

reverse?: boolean,

21

orientation?: "horizontal" | "vertical"

22

): number;

23

24

/**

25

* Gets the document that owns an element

26

* @param el - Element to get document for

27

* @returns Element's owner document or global document

28

*/

29

function getOwnerDocument(el: Element): Document;

30

31

/**

32

* Gets the window that owns an element

33

* @param el - Element to get window for

34

* @returns Element's owner window or global window

35

*/

36

function getOwnerWindow(el: Element): Window;

37

38

/**

39

* Type guard to check if node is a ShadowRoot

40

* @param node - Node to check

41

* @returns Boolean with proper type narrowing

42

*/

43

function isShadowRoot(node: Node): node is ShadowRoot;

44

```

45

46

**Usage Examples:**

47

48

```typescript

49

import { getOffset, getOwnerDocument, getOwnerWindow, isShadowRoot } from "@react-aria/utils";

50

51

function PositionTracker({ children }) {

52

const elementRef = useRef<HTMLDivElement>(null);

53

const [position, setPosition] = useState({ x: 0, y: 0 });

54

55

useEffect(() => {

56

if (!elementRef.current) return;

57

58

const updatePosition = () => {

59

const element = elementRef.current!;

60

61

// Get horizontal and vertical offsets

62

const x = getOffset(element, false, 'horizontal');

63

const y = getOffset(element, false, 'vertical');

64

65

setPosition({ x, y });

66

};

67

68

// Get the appropriate window for event listeners

69

const ownerWindow = getOwnerWindow(elementRef.current);

70

71

updatePosition();

72

ownerWindow.addEventListener('scroll', updatePosition);

73

ownerWindow.addEventListener('resize', updatePosition);

74

75

return () => {

76

ownerWindow.removeEventListener('scroll', updatePosition);

77

ownerWindow.removeEventListener('resize', updatePosition);

78

};

79

}, []);

80

81

return (

82

<div ref={elementRef}>

83

<p>Position: ({position.x}, {position.y})</p>

84

{children}

85

</div>

86

);

87

}

88

89

// Cross-frame DOM utilities

90

function CrossFrameComponent({ targetFrame }) {

91

const [targetDocument, setTargetDocument] = useState<Document | null>(null);

92

93

useEffect(() => {

94

if (targetFrame && targetFrame.contentDocument) {

95

const frameDoc = targetFrame.contentDocument;

96

setTargetDocument(frameDoc);

97

98

// Use owner document utilities for frame elements

99

const frameElements = frameDoc.querySelectorAll('.interactive');

100

frameElements.forEach(element => {

101

const ownerDoc = getOwnerDocument(element);

102

const ownerWin = getOwnerWindow(element);

103

104

console.log('Element owner document:', ownerDoc === frameDoc);

105

console.log('Element owner window:', ownerWin === targetFrame.contentWindow);

106

});

107

}

108

}, [targetFrame]);

109

110

return <div>Cross-frame utilities active</div>;

111

}

112

113

// Shadow DOM detection

114

function ShadowDOMHandler({ rootElement }) {

115

useEffect(() => {

116

if (!rootElement) return;

117

118

const walker = document.createTreeWalker(

119

rootElement,

120

NodeFilter.SHOW_ELEMENT,

121

{

122

acceptNode: (node) => {

123

// Check if we've encountered a shadow root

124

if (isShadowRoot(node)) {

125

console.log('Found shadow root:', node);

126

return NodeFilter.FILTER_ACCEPT;

127

}

128

return NodeFilter.FILTER_SKIP;

129

}

130

}

131

);

132

133

let currentNode;

134

while (currentNode = walker.nextNode()) {

135

// Process shadow roots

136

handleShadowRoot(currentNode as ShadowRoot);

137

}

138

}, [rootElement]);

139

140

return null;

141

}

142

```

143

144

### Value Management

145

146

Utilities for managing values and preventing unnecessary re-renders.

147

148

```typescript { .api }

149

/**

150

* Creates an inert reference to a value

151

* @param value - Value to make inert

152

* @returns Inert value reference that doesn't trigger re-renders

153

*/

154

function inertValue<T>(value: T): T;

155

```

156

157

**Usage Examples:**

158

159

```typescript

160

import { inertValue } from "@react-aria/utils";

161

162

function ExpensiveComponent({ data, onProcess }) {

163

// Create inert reference to prevent unnecessary recalculations

164

const inertData = inertValue(data);

165

166

const processedData = useMemo(() => {

167

// Expensive processing that should only run when data actually changes

168

return expensiveProcessing(inertData);

169

}, [inertData]);

170

171

return <div>{processedData.summary}</div>;

172

}

173

174

// Stable callback references

175

function CallbackComponent({ onChange }) {

176

// Make callback inert to prevent effect dependencies

177

const inertOnChange = inertValue(onChange);

178

179

useEffect(() => {

180

// This effect won't re-run when onChange reference changes

181

const subscription = subscribe(inertOnChange);

182

return () => subscription.unsubscribe();

183

}, [inertOnChange]);

184

185

return <div>Component with stable callback</div>;

186

}

187

```

188

189

### Constants

190

191

Pre-defined constants used by React Aria components.

192

193

```typescript { .api }

194

/**

195

* Custom event name for clearing focus in autocomplete

196

*/

197

const CLEAR_FOCUS_EVENT = "react-aria-clear-focus";

198

199

/**

200

* Custom event name for setting focus in autocomplete

201

*/

202

const FOCUS_EVENT = "react-aria-focus";

203

```

204

205

**Usage Examples:**

206

207

```typescript

208

import { CLEAR_FOCUS_EVENT, FOCUS_EVENT } from "@react-aria/utils";

209

210

function CustomAutocomplete({ suggestions, onSelect }) {

211

const inputRef = useRef<HTMLInputElement>(null);

212

const listRef = useRef<HTMLUListElement>(null);

213

214

const clearFocus = () => {

215

// Dispatch custom clear focus event

216

const event = new CustomEvent(CLEAR_FOCUS_EVENT, {

217

bubbles: true,

218

detail: { target: inputRef.current }

219

});

220

221

inputRef.current?.dispatchEvent(event);

222

};

223

224

const focusOption = (optionElement: HTMLElement) => {

225

// Dispatch custom focus event

226

const event = new CustomEvent(FOCUS_EVENT, {

227

bubbles: true,

228

detail: { target: optionElement }

229

});

230

231

optionElement.dispatchEvent(event);

232

};

233

234

useEffect(() => {

235

const input = inputRef.current;

236

if (!input) return;

237

238

const handleClearFocus = (e: CustomEvent) => {

239

console.log('Clear focus requested:', e.detail.target);

240

input.blur();

241

};

242

243

const handleFocus = (e: CustomEvent) => {

244

console.log('Focus requested:', e.detail.target);

245

e.detail.target.focus();

246

};

247

248

input.addEventListener(CLEAR_FOCUS_EVENT, handleClearFocus);

249

listRef.current?.addEventListener(FOCUS_EVENT, handleFocus);

250

251

return () => {

252

input.removeEventListener(CLEAR_FOCUS_EVENT, handleClearFocus);

253

listRef.current?.removeEventListener(FOCUS_EVENT, handleFocus);

254

};

255

}, []);

256

257

return (

258

<div>

259

<input ref={inputRef} />

260

<ul ref={listRef}>

261

{suggestions.map((suggestion, index) => (

262

<li

263

key={index}

264

onClick={() => focusOption(inputRef.current!)}

265

>

266

{suggestion}

267

</li>

268

))}

269

</ul>

270

<button onClick={clearFocus}>Clear Focus</button>

271

</div>

272

);

273

}

274

```

275

276

### Drag & Drop (Deprecated)

277

278

One-dimensional drag gesture utility (deprecated in favor of newer alternatives).

279

280

```typescript { .api }

281

/**

282

* ⚠️ DEPRECATED - Use @react-aria/interactions useMove instead

283

* 1D dragging behavior for sliders and similar components

284

* @param props - Configuration for drag behavior

285

* @returns Event handlers for drag functionality

286

*/

287

function useDrag1D(props: {

288

containerRef?: RefObject<Element>;

289

reverse?: boolean;

290

orientation?: "horizontal" | "vertical";

291

onDrag?: (e: { deltaX: number; deltaY: number }) => void;

292

onDragStart?: (e: PointerEvent) => void;

293

onDragEnd?: (e: PointerEvent) => void;

294

}): HTMLAttributes<HTMLElement>;

295

```

296

297

**Usage Examples:**

298

299

```typescript

300

import { useDrag1D } from "@react-aria/utils";

301

302

// ⚠️ This is deprecated - use @react-aria/interactions instead

303

function DeprecatedSlider({ value, onChange, min = 0, max = 100 }) {

304

const containerRef = useRef<HTMLDivElement>(null);

305

306

const dragProps = useDrag1D({

307

containerRef,

308

orientation: 'horizontal',

309

onDrag: ({ deltaX }) => {

310

const container = containerRef.current;

311

if (!container) return;

312

313

const containerWidth = container.offsetWidth;

314

const deltaValue = (deltaX / containerWidth) * (max - min);

315

const newValue = Math.max(min, Math.min(max, value + deltaValue));

316

317

onChange(newValue);

318

}

319

});

320

321

return (

322

<div ref={containerRef} className="slider" {...dragProps}>

323

<div className="slider-track">

324

<div

325

className="slider-thumb"

326

style={{ left: `${((value - min) / (max - min)) * 100}%` }}

327

/>

328

</div>

329

</div>

330

);

331

}

332

```

333

334

### Load More Utilities

335

336

Utilities for implementing infinite scrolling and pagination.

337

338

```typescript { .api }

339

/**

340

* Manages infinite scrolling and load more functionality

341

* @param options - Configuration for load more behavior

342

* @returns Load more state and controls

343

*/

344

function useLoadMore<T>(options: {

345

items: T[];

346

onLoadMore: () => Promise<T[]> | void;

347

isLoading?: boolean;

348

hasMore?: boolean;

349

}): {

350

items: T[];

351

isLoading: boolean;

352

hasMore: boolean;

353

loadMore: () => void;

354

};

355

356

/**

357

* Uses IntersectionObserver to trigger load more when sentinel is visible

358

* @param props - Configuration for sentinel behavior

359

* @param ref - RefObject to sentinel element

360

*/

361

function useLoadMoreSentinel(

362

props: LoadMoreSentinelProps,

363

ref: RefObject<HTMLElement>

364

): void;

365

366

/**

367

* Unstable version of useLoadMoreSentinel

368

* @deprecated Use useLoadMoreSentinel instead

369

*/

370

const UNSTABLE_useLoadMoreSentinel = useLoadMoreSentinel;

371

372

interface LoadMoreSentinelProps {

373

collection: any;

374

onLoadMore: () => void;

375

scrollOffset?: number;

376

}

377

```

378

379

**Usage Examples:**

380

381

```typescript

382

import { useLoadMore, useLoadMoreSentinel } from "@react-aria/utils";

383

384

function InfiniteList({ initialItems, loadMoreItems }) {

385

const {

386

items,

387

isLoading,

388

hasMore,

389

loadMore

390

} = useLoadMore({

391

items: initialItems,

392

onLoadMore: async () => {

393

const newItems = await loadMoreItems();

394

return newItems;

395

},

396

hasMore: true

397

});

398

399

const sentinelRef = useRef<HTMLDivElement>(null);

400

401

useLoadMoreSentinel({

402

collection: items,

403

onLoadMore: loadMore,

404

scrollOffset: 0.8 // Trigger when 80% scrolled

405

}, sentinelRef);

406

407

return (

408

<div className="infinite-list">

409

{items.map(item => (

410

<div key={item.id}>{item.name}</div>

411

))}

412

413

{hasMore && (

414

<div ref={sentinelRef} className="loading-sentinel">

415

{isLoading ? 'Loading...' : 'Load more'}

416

</div>

417

)}

418

</div>

419

);

420

}

421

```

422

423

### Re-exported Utilities

424

425

Mathematical utilities re-exported from @react-stately/utils.

426

427

```typescript { .api }

428

/**

429

* Clamps value between min and max bounds

430

* @param value - Value to clamp

431

* @param min - Minimum value

432

* @param max - Maximum value

433

* @returns Clamped value

434

*/

435

function clamp(value: number, min: number, max: number): number;

436

437

/**

438

* Snaps value to nearest step increment

439

* @param value - Value to snap

440

* @param step - Step increment

441

* @param min - Optional minimum value

442

* @returns Snapped value

443

*/

444

function snapValueToStep(value: number, step: number, min?: number): number;

445

```

446

447

**Usage Examples:**

448

449

```typescript

450

import { clamp, snapValueToStep } from "@react-aria/utils";

451

452

function NumericInput({ value, onChange, min = 0, max = 100, step = 1 }) {

453

const handleChange = (newValue: number) => {

454

// Clamp to bounds and snap to step

455

const clampedValue = clamp(newValue, min, max);

456

const snappedValue = snapValueToStep(clampedValue, step, min);

457

458

onChange(snappedValue);

459

};

460

461

return (

462

<input

463

type="range"

464

value={value}

465

min={min}

466

max={max}

467

step={step}

468

onChange={(e) => handleChange(Number(e.target.value))}

469

/>

470

);

471

}

472

473

// Color picker with HSL snapping

474

function ColorPicker({ hue, saturation, lightness, onChange }) {

475

const handleHueChange = (newHue: number) => {

476

// Snap hue to 15-degree increments

477

const snappedHue = snapValueToStep(newHue, 15);

478

const clampedHue = clamp(snappedHue, 0, 360);

479

480

onChange({ hue: clampedHue, saturation, lightness });

481

};

482

483

const handleSaturationChange = (newSaturation: number) => {

484

// Snap saturation to 5% increments

485

const snappedSat = snapValueToStep(newSaturation, 5);

486

const clampedSat = clamp(snappedSat, 0, 100);

487

488

onChange({ hue, saturation: clampedSat, lightness });

489

};

490

491

return (

492

<div>

493

<input

494

type="range"

495

min={0}

496

max={360}

497

step={15}

498

value={hue}

499

onChange={(e) => handleHueChange(Number(e.target.value))}

500

/>

501

<input

502

type="range"

503

min={0}

504

max={100}

505

step={5}

506

value={saturation}

507

onChange={(e) => handleSaturationChange(Number(e.target.value))}

508

/>

509

</div>

510

);

511

}

512

513

// Animation easing with step snapping

514

function useAnimationValue(targetValue: number, duration = 1000) {

515

const [currentValue, setCurrentValue] = useState(0);

516

517

useEffect(() => {

518

let startTime: number;

519

let animationFrame: number;

520

521

const animate = (timestamp: number) => {

522

if (!startTime) startTime = timestamp;

523

524

const progress = clamp((timestamp - startTime) / duration, 0, 1);

525

526

// Ease-out function

527

const easedProgress = 1 - Math.pow(1 - progress, 3);

528

529

// Calculate intermediate value and snap to steps

530

const intermediateValue = easedProgress * targetValue;

531

const snappedValue = snapValueToStep(intermediateValue, 0.1);

532

533

setCurrentValue(snappedValue);

534

535

if (progress < 1) {

536

animationFrame = requestAnimationFrame(animate);

537

}

538

};

539

540

animationFrame = requestAnimationFrame(animate);

541

542

return () => {

543

if (animationFrame) {

544

cancelAnimationFrame(animationFrame);

545

}

546

};

547

}, [targetValue, duration]);

548

549

return currentValue;

550

}

551

```

552

553

### Utility Combinations

554

555

Real-world examples combining multiple miscellaneous utilities:

556

557

```typescript

558

import {

559

getOffset,

560

getOwnerWindow,

561

clamp,

562

snapValueToStep,

563

inertValue

564

} from "@react-aria/utils";

565

566

function DraggableSlider({ value, onChange, min = 0, max = 100, step = 1 }) {

567

const sliderRef = useRef<HTMLDivElement>(null);

568

const [isDragging, setIsDragging] = useState(false);

569

570

// Make onChange inert to prevent unnecessary effect re-runs

571

const inertOnChange = inertValue(onChange);

572

573

const handleMouseMove = useCallback((e: MouseEvent) => {

574

if (!isDragging || !sliderRef.current) return;

575

576

// Get slider position and dimensions

577

const slider = sliderRef.current;

578

const sliderRect = slider.getBoundingClientRect();

579

const sliderOffset = getOffset(slider, false, 'horizontal');

580

581

// Calculate relative position

582

const relativeX = e.clientX - sliderRect.left;

583

const percentage = clamp(relativeX / sliderRect.width, 0, 1);

584

585

// Convert to value and snap to step

586

const rawValue = min + percentage * (max - min);

587

const snappedValue = snapValueToStep(rawValue, step, min);

588

const finalValue = clamp(snappedValue, min, max);

589

590

inertOnChange(finalValue);

591

}, [isDragging, min, max, step, inertOnChange]);

592

593

useEffect(() => {

594

if (!isDragging) return;

595

596

const ownerWindow = sliderRef.current

597

? getOwnerWindow(sliderRef.current)

598

: window;

599

600

ownerWindow.addEventListener('mousemove', handleMouseMove);

601

ownerWindow.addEventListener('mouseup', () => setIsDragging(false));

602

603

return () => {

604

ownerWindow.removeEventListener('mousemove', handleMouseMove);

605

ownerWindow.removeEventListener('mouseup', () => setIsDragging(false));

606

};

607

}, [isDragging, handleMouseMove]);

608

609

const handlePercent = clamp((value - min) / (max - min), 0, 1);

610

611

return (

612

<div

613

ref={sliderRef}

614

className="slider"

615

onMouseDown={() => setIsDragging(true)}

616

>

617

<div

618

className="slider-thumb"

619

style={{ left: `${handlePercent * 100}%` }}

620

/>

621

</div>

622

);

623

}

624

```