Skip to content

feat: improve slot system consistency#7869

Open
adierkens wants to merge 1 commit into
mainfrom
adierkens/slot-fixes
Open

feat: improve slot system consistency#7869
adierkens wants to merge 1 commit into
mainfrom
adierkens/slot-fixes

Conversation

@adierkens
Copy link
Copy Markdown
Contributor

@adierkens adierkens commented May 22, 2026

Closes #

Improves consistency across the slot system (__SLOT__ markers + useSlots / isSlot). Three buckets of work:

  1. Cleanup: remove orphan root markers, standardise Symbol(...) descriptions, migrate hand-rolled Children.toArray + isSlot consumers to useSlots where the pattern fits.
  2. Public API: export the slot primitives that downstream consumers currently hand-roll, plus a new asSlot helper that replaces the cast-heavy marker-copy pattern.
  3. Documentation: a contributor skill at .github/skills/slots/SKILL.md that describes when and how to use slots, with naming conventions and pitfalls.

Changelog

New

  • Public exports from @primer/react: useSlots, isSlot, asSlot, WithSlotMarker, FCWithSlotMarker (alongside the existing SlotMarker type). useSlots is also exported from @primer/react/hooks.
  • asSlot(component, slotSource) helper that copies a __SLOT__ marker from a source slot component onto a wrapper. Typed; dev-warns when the source has no marker. Replaces the cast-heavy (Wrapper as typeof Wrapper & {__SLOT__?: symbol}).__SLOT__ = Source.__SLOT__ pattern.
  • Dev-mode warning in useSlots when a child's displayName matches a slot component's displayName but the child is missing the __SLOT__ marker (the most common wrapping footgun).
  • .github/skills/slots/SKILL.md contributor skill documenting when to use slots, naming conventions, public APIs, limitations, and common pitfalls. Referenced from .github/copilot-instructions.md.

Changed

  • Standardised Symbol(...) descriptions used as slot markers to the Parent.Slot convention: CheckboxOrRadioGroup.Label, CheckboxOrRadioGroup.Caption, CheckboxOrRadioGroup.Validation, Tooltip (was DEPRECATED_Tooltip), DataTable.Table (was Table).
  • Migrated PageHeader, NavList.Item, and the internal CheckboxOrRadioGroup to use useSlots instead of hand-rolled React.Children traversals. The CheckboxOrRadioGroup migration also removes duplicated work where useSlots was already being called but slots were re-extracted by hand immediately after.

Removed

  • Orphan __SLOT__ markers from 7 root components with no internal consumers: ActionMenu (root Menu), UnderlinePanels, PageLayout, SegmentedControl, RadioGroup, CheckboxGroup, and Dialog. Sub-component markers are intentionally retained so consumers can keep wrapping them.

What was evaluated but intentionally not migrated

Four hand-rolled consumers use patterns that don't fit useSlots's single-match-per-key model:

  • CheckboxGroup and UnderlinePanels — multi-match (find all children of a given type). Would migrate cleanly if useSlots grew a multi-match option; design TBD in a follow-up.
  • SegmentedControl — per-child classification with index tracking (selection state). Not a slot extraction pattern.
  • ActionMenuChildren.map with side effects, including grandchild introspection for Tooltip-wrapped Anchors. Not a slot extraction pattern.

TreeView's useSubTree is also a clean single-match consumer but lives inside a useMemo; migrating tripped react-hooks/rules-of-hooks (the lint rule enforces the use* naming convention even though useSlots is a plain function under the hood). Left as-is. The slots skill calls this pitfall out explicitly.

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

New public exports + new helper, no breaking changes. The removed root __SLOT__ markers had no consumers anywhere in the codebase (verified by grep) so removing them is non-breaking; if a downstream consumer was secretly wrapping against e.g. Dialog.__SLOT__, they would now lose the slot match — but Dialog is a root and nothing scanned for it, so there was nothing to be matched into.

Testing & Reviewing

  • Type-check, ESLint, and prettier --check all clean on changed files.
  • 478 browser-mode tests passed across the affected components (utils, hooks, PageHeader, NavList, Dialog, CheckboxGroup, RadioGroup, SegmentedControl, ActionMenu, UnderlinePanels, Tooltip, FormControl, Autocomplete, TreeView, internal CheckboxOrRadioGroup).
  • The node-mode exports.test.ts snapshot was updated to include the new public exports.
  • PageLayout snapshot mismatches exist on the unchanged baseline (CSS hash drift) and are not caused by this PR — easy to verify by stashing and re-running.

Areas worth a close look during review:

  • packages/react/src/hooks/useSlots.ts — the new warnIfDisplayNameMatchesWithoutMarker helper is dev-only but runs once per non-matching child; happy to gate it behind a feature flag if perf is a concern.
  • packages/react/src/PageHeader/PageHeader.tsxuseSlots is now called at the top level even though the only consumer is a dev-only effect. Couldn't keep it inside the effect because of react-hooks/rules-of-hooks. PageHeader isn't render-hot so this should be fine, but flagging.
  • .github/skills/slots/SKILL.md — convention statements; let me know if anything contradicts your intent.

Merge checklist

- Remove orphan __SLOT__ markers from root components with no consumers:
  ActionMenu (Menu), UnderlinePanels, PageLayout, SegmentedControl,
  RadioGroup, CheckboxGroup, and Dialog.
- Standardize Symbol() descriptions to the Parent.Slot convention:
  CheckboxOrRadioGroup.{Label,Caption,Validation}, Tooltip (was
  DEPRECATED_Tooltip), and DataTable.Table (was Table).
- Migrate PageHeader, NavList.Item, and the internal CheckboxOrRadioGroup
  to useSlots instead of hand-rolled Children.toArray + isSlot loops.
  The CheckboxOrRadioGroup migration also drops duplicated work where
  useSlots was already called but slots were re-extracted by hand.
- Export useSlots, isSlot, asSlot, WithSlotMarker, and FCWithSlotMarker
  publicly from @primer/react (also useSlots from @primer/react/hooks).
- Add asSlot(component, slotSource) helper that copies a __SLOT__ marker
  from a source slot component onto a wrapper, replacing the cast-heavy
  manual marker-copy pattern. Dev-mode warns when the source has no marker.
- Add a dev-mode displayName-mismatch warning in useSlots that catches
  wrappers around slot components that forgot to copy the marker.
- Add .github/skills/slots/SKILL.md documenting when to use slots, naming
  conventions, public APIs, and common pitfalls. Reference it from
  copilot-instructions.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 22, 2026

🦋 Changeset detected

Latest commit: 54ebf65

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label May 22, 2026
@github-actions
Copy link
Copy Markdown
Contributor

⚠️ Action required

👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. Check the integration testing docs for step-by-step instructions. Or, apply the integration-tests: skipped manually label to skip these checks.

To publish a canary release for integration testing, apply the Canary Release label to this PR.

@adierkens adierkens added the Canary Release Apply this label when you want CI to create a canary release of the current PR label May 22, 2026
@adierkens adierkens added Canary Release Apply this label when you want CI to create a canary release of the current PR and removed Canary Release Apply this label when you want CI to create a canary release of the current PR labels May 22, 2026
@adierkens adierkens marked this pull request as ready for review May 22, 2026 20:14
@adierkens adierkens requested a review from a team as a code owner May 22, 2026 20:14
@adierkens adierkens requested review from Copilot and siddharthkp May 22, 2026 20:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves consistency and ergonomics of Primer React’s slot system by standardizing __SLOT__ markers, migrating a few internal consumers to useSlots, and exposing slot primitives as public API (plus a new asSlot helper). It also adds contributor documentation for slots and updates the public exports snapshot accordingly.

Changes:

  • Add/export slot utilities (useSlots, isSlot, asSlot) and slot marker types (WithSlotMarker, FCWithSlotMarker) from @primer/react (+ useSlots from @primer/react/hooks), with tests and export snapshot updates.
  • Standardize slot marker symbol descriptions (e.g. Parent.Slot convention) and remove orphan root __SLOT__ markers from components that aren’t scanned as children.
  • Improve slot usage consistency by migrating selected components to useSlots and adding a new dev-only warning path in useSlots.
Show a summary per file
File Description
packages/react/src/utils/as-slot.ts Adds asSlot helper to copy __SLOT__ markers onto wrapper components.
packages/react/src/utils/tests/as-slot.test.tsx Adds unit tests for asSlot marker copying + dev warning.
packages/react/src/hooks/useSlots.ts Adds a dev-only warning for displayName matches that are missing a __SLOT__ marker.
packages/react/src/hooks/index.ts Re-exports useSlots from the hooks entrypoint.
packages/react/src/index.ts Publicly exports slot primitives/types (useSlots, isSlot, asSlot, etc.).
packages/react/src/tests/snapshots/exports.test.ts.snap Updates exports snapshot for newly public exports.
packages/react/src/PageHeader/PageHeader.tsx Migrates slot detection from manual Children traversal to useSlots for dev-only validation logic.
packages/react/src/NavList/NavList.tsx Migrates SubNav/TrailingAction extraction to useSlots.
packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroup.tsx Removes redundant manual slot extraction in favor of the existing useSlots result.
packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroupLabel.tsx Standardizes slot symbol description to CheckboxOrRadioGroup.Label.
packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroupCaption.tsx Standardizes slot symbol description to CheckboxOrRadioGroup.Caption.
packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroupValidation.tsx Standardizes slot symbol description to CheckboxOrRadioGroup.Validation.
packages/react/src/Tooltip/Tooltip.tsx Renames Tooltip slot symbol description to Tooltip.
packages/react/src/DataTable/index.ts Renames Table slot symbol description to DataTable.Table.
packages/react/src/ActionMenu/ActionMenu.tsx Removes orphan root __SLOT__ marker from ActionMenu root.
packages/react/src/Dialog/Dialog.tsx Removes orphan root __SLOT__ marker from Dialog root.
packages/react/src/PageLayout/PageLayout.tsx Removes orphan root __SLOT__ marker from PageLayout root.
packages/react/src/SegmentedControl/SegmentedControl.tsx Removes orphan root __SLOT__ marker from SegmentedControl root.
packages/react/src/RadioGroup/RadioGroup.tsx Removes orphan root __SLOT__ marker from RadioGroup root.
packages/react/src/CheckboxGroup/CheckboxGroup.tsx Removes orphan root __SLOT__ marker from CheckboxGroup root.
packages/react/src/experimental/UnderlinePanels/UnderlinePanels.tsx Removes orphan root __SLOT__ marker from UnderlinePanels root (retains sub-slots).
.github/skills/slots/SKILL.md Adds contributor documentation (“skill”) for slot conventions and public APIs.
.github/copilot-instructions.md References the new slots skill doc from Copilot instructions.
.changeset/slot-system-consistency.md Adds a minor changeset describing slot system API + consistency changes.

Copilot's findings

  • Files reviewed: 24/24 changed files
  • Comments generated: 3

Comment on lines +22 to +28
if (__DEV__) {
warning(
!slotSource.__SLOT__,
'asSlot: the provided slotSource does not have a `__SLOT__` marker. The wrapper will not be recognised by the parent slot scanner.',
)
}
;(component as unknown as SlotMarker).__SLOT__ = slotSource.__SLOT__
Comment on lines +16 to +20
beforeEach(() => {
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
return () => {
warnSpy.mockRestore()
}
})
```

`isSlot` always checks both the direct type and the slot marker so wrappers built with `asSlot` are recognised.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Canary Release Apply this label when you want CI to create a canary release of the current PR integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants