@primer/react
Version:
An implementation of GitHub's Primer Design System using React
151 lines (134 loc) • 4.94 kB
JavaScript
import React from 'react';
import { warning } from '../utils/warning.js';
import { isSlot } from '../utils/is-slot.js';
/**
* useSlots - Extract slot components from children for SSR-compatible slot APIs.
*
* Given a list of children and a config mapping slot names to component types,
* separates children into two groups: matched slots and the rest.
*
* Config: { leadingVisual: LeadingVisual, description: Description }
*
* Children: Matching: Output:
* +----------------+ slots = {
* | LeadingVisual | -> matches slot 0 --> leadingVisual: <LeadingVisual />
* | "Project name" | -> no match --> description: <Description />
* | Description | -> matches slot 1 --> }
* | TrailingVisual | -> no match --> rest = ["Project name", <TrailingVisual />]
* +----------------+
*
* Performance-sensitive: called per item in lists (e.g. 100-item ActionList
* calls this 101 times per render).
*
* Once all slots are filled, remaining children skip matching entirely in
* production (single integer comparison). In dev, we still scan for duplicates
* to emit a warning.
*
* Flow per child:
*
* child ──> isValidElement? ──no──> rest[]
* |
* yes
* |
* v
* all slots filled? ──yes──> rest[] (prod)
* | |
* no [dev: check for
* | duplicate warning]
* v
* match against unfilled slots
* |
* found match? ──no──> rest[]
* |
* yes
* |
* v
* slots[key] = child, slotsFound++
*
* Slot config supports two matcher styles:
* 1. Component reference: { visual: LeadingVisual }
* 2. Component + test fn: { block: [Description, props => props.variant === 'block'] }
*/
// --- Types ---
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// --- Matching ---
/** Check if a child element matches a slot config entry, either by direct type comparison or slot symbol. */
function childMatchesSlot(child, slotValue) {
if (Array.isArray(slotValue)) {
const [component, testFn] = slotValue;
return (child.type === component || isSlot(child, component)) && testFn(child.props);
}
return child.type === slotValue || isSlot(child, slotValue);
}
// --- Hook ---
/** Extract slot components from children. See file header for details. */
function useSlots(children, config) {
const rest = [];
const keys = Object.keys(config);
const values = Object.values(config);
const totalSlots = keys.length;
// Initialize all slot keys to undefined so callers can check `slots.x === undefined`
const slots = {};
for (let i = 0; i < totalSlots; i++) {
slots[keys[i]] = undefined;
}
let slotsFound = 0;
// eslint-disable-next-line github/array-foreach
React.Children.forEach(children, child => {
if (! /*#__PURE__*/React.isValidElement(child)) {
rest.push(child);
return;
}
// Short-circuit: all slots filled, no more matching needed
if (slotsFound === totalSlots) {
if (process.env.NODE_ENV !== "production" && warnIfDuplicate(child, keys, values, totalSlots)) {
return;
}
rest.push(child);
return;
}
// Try to match child against a slot
const matchedIndex = findMatchingSlot(child, values, totalSlots);
if (matchedIndex === -1) {
rest.push(child);
return;
}
const slotKey = keys[matchedIndex];
// Duplicate: slot already filled by an earlier child
if (slots[slotKey] !== undefined) {
if (process.env.NODE_ENV !== "production") {
process.env.NODE_ENV !== "production" ? warning(true, `Found duplicate "${String(slotKey)}" slot. Only the first will be rendered.`) : void 0;
}
return;
}
slots[slotKey] = child;
slotsFound++;
});
return [slots, rest];
}
// --- Helpers ---
/**
* Find the first slot config entry matching this child.
* Returns the config index, or -1 if no match.
*/
function findMatchingSlot(child, values, totalSlots) {
for (let i = 0; i < totalSlots; i++) {
if (childMatchesSlot(child, values[i])) return i;
}
return -1;
}
/**
* Dev-only: check if a child duplicates an already-filled slot.
* Returns true (and warns) if a duplicate is found, false otherwise.
* Used in the short-circuit path where all slots are already filled.
*/
function warnIfDuplicate(child, keys, values, totalSlots) {
for (let i = 0; i < totalSlots; i++) {
if (childMatchesSlot(child, values[i])) {
process.env.NODE_ENV !== "production" ? warning(true, `Found duplicate "${String(keys[i])}" slot. Only the first will be rendered.`) : void 0;
return true;
}
}
return false;
}
export { useSlots };