UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

137 lines (136 loc) 5.18 kB
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 };