@primer/react
Version:
An implementation of GitHub's Primer Design System using React
137 lines (136 loc) • 5.18 kB
JavaScript
import { warning } from "../utils/warning.js";
import { isSlot } from "../utils/is-slot.js";
import React from "react";
//#region src/hooks/useSlots.ts
/**
* 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'] }
*/
/** 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);
}
/** 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;
const slots = {};
for (let i = 0; i < totalSlots; i++) slots[keys[i]] = void 0;
let slotsFound = 0;
React.Children.forEach(children, (child) => {
if (!/*#__PURE__*/ React.isValidElement(child)) {
rest.push(child);
return;
}
if (slotsFound === totalSlots) {
if (warnIfDuplicate(child, keys, values, totalSlots)) return;
rest.push(child);
return;
}
const matchedIndex = findMatchingSlot(child, values, totalSlots);
if (matchedIndex === -1) {
warnIfDisplayNameMatchesWithoutMarker(child, keys, values, totalSlots);
rest.push(child);
return;
}
const slotKey = keys[matchedIndex];
if (slots[slotKey] !== void 0) {
warning(true, `Found duplicate "${String(slotKey)}" slot. Only the first will be rendered.`);
return;
}
slots[slotKey] = child;
slotsFound++;
});
return [slots, rest];
}
/**
* 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])) {
warning(true, `Found duplicate "${String(keys[i])}" slot. Only the first will be rendered.`);
return true;
}
return false;
}
/**
* Dev-only: detect children whose displayName matches a slot's component
* displayName but that don't share the `__SLOT__` marker. Catches a common
* footgun where a wrapper around a slot component forgets to copy the marker.
*/
function warnIfDisplayNameMatchesWithoutMarker(child, keys, values, totalSlots) {
const childType = child.type;
if (typeof childType !== "function" && typeof childType !== "object") return;
const childName = childType.displayName;
if (!childName) return;
for (let i = 0; i < totalSlots; i++) {
const slotValue = values[i];
const slotName = (Array.isArray(slotValue) ? slotValue[0] : slotValue).displayName;
if (slotName && slotName === childName) {
warning(true, `useSlots: child with displayName "${childName}" matches slot "${String(keys[i])}" by name but is missing the \`__SLOT__\` marker. Did you forget to copy the marker? Use \`asSlot(wrapper, ParentSlotComponent)\` to copy it.`);
return;
}
}
}
//#endregion
export { useSlots };