diff --git a/packages/react/src/ActionList/Group.tsx b/packages/react/src/ActionList/Group.tsx index df319996f67..5d981a41947 100644 --- a/packages/react/src/ActionList/Group.tsx +++ b/packages/react/src/ActionList/Group.tsx @@ -10,6 +10,7 @@ import groupClasses from './Group.module.css' import type {FCWithSlotMarker} from '../utils/types/Slots' import {GroupHeadingTrailingAction} from './GroupHeadingTrailingAction' import {useFeatureFlag} from '../FeatureFlags' +import {GroupContext} from './GroupContext' const GROUP_HEADING_TRAILING_ACTION_FEATURE_FLAG = 'primer_react_action_list_group_heading_trailing_action' @@ -66,12 +67,6 @@ export type ActionListGroupProps = React.HTMLAttributes & { selectionVariant?: ActionListProps['selectionVariant'] | false } -type ContextProps = Pick & {groupHeadingId: string | undefined} -export const GroupContext = React.createContext({ - groupHeadingId: undefined, - selectionVariant: undefined, -}) - export const Group: FCWithSlotMarker> = ({ title, variant = 'subtle', diff --git a/packages/react/src/ActionList/GroupContext.ts b/packages/react/src/ActionList/GroupContext.ts new file mode 100644 index 00000000000..48195b63e5f --- /dev/null +++ b/packages/react/src/ActionList/GroupContext.ts @@ -0,0 +1,9 @@ +import React from 'react' +import type {ActionListGroupProps} from './Group' + +type ContextProps = Pick & {groupHeadingId: string | undefined} + +export const GroupContext = React.createContext({ + groupHeadingId: undefined, + selectionVariant: undefined, +}) diff --git a/packages/react/src/ActionList/Item.tsx b/packages/react/src/ActionList/Item.tsx index 1181f90c110..355dd463494 100644 --- a/packages/react/src/ActionList/Item.tsx +++ b/packages/react/src/ActionList/Item.tsx @@ -3,7 +3,7 @@ import {useId} from '../hooks/useId' import {useSlots} from '../hooks/useSlots' import {ActionListContainerContext} from './ActionListContainerContext' import {Description} from './Description' -import {GroupContext} from './Group' +import {GroupContext} from './GroupContext' import type {ActionListItemProps, ActionListProps} from './shared' import {Selection} from './Selection' import {LeadingVisual, TrailingVisual, VisualOrIndicator} from './Visuals' @@ -16,7 +16,7 @@ import classes from './ActionList.module.css' import {clsx} from 'clsx' import {fixedForwardRef} from '../utils/modern-polymorphic' import {Tooltip} from '../TooltipV2' -import {TooltipContext} from '../TooltipV2/Tooltip' +import {TooltipContext} from '../TooltipV2/TooltipContext' type ActionListSubItemProps = { children?: React.ReactNode diff --git a/packages/react/src/ActionList/Selection.tsx b/packages/react/src/ActionList/Selection.tsx index 6fbab8c20ee..3321ff90e44 100644 --- a/packages/react/src/ActionList/Selection.tsx +++ b/packages/react/src/ActionList/Selection.tsx @@ -1,7 +1,7 @@ import React from 'react' import {CheckIcon} from '@primer/octicons-react' import type {ActionListGroupProps} from './Group' -import {GroupContext} from './Group' +import {GroupContext} from './GroupContext' import {type ActionListProps, type ActionListItemProps, ListContext} from './shared' import {VisualContainer} from './Visuals' import classes from './ActionList.module.css' diff --git a/packages/react/src/ActionList/index.ts b/packages/react/src/ActionList/index.ts index b07f9d9d9db..eb9cfbfcaa8 100644 --- a/packages/react/src/ActionList/index.ts +++ b/packages/react/src/ActionList/index.ts @@ -1,5 +1,6 @@ import {List} from './List' -import {Group, GroupContext, GroupHeading} from './Group' +import {Group, GroupHeading} from './Group' +import {GroupContext} from './GroupContext' import {Item} from './Item' import {LinkItem} from './LinkItem' import {Divider} from './Divider' diff --git a/packages/react/src/ActionMenu/ActionMenu.tsx b/packages/react/src/ActionMenu/ActionMenu.tsx index 7762d0307ee..6e95c622c5a 100644 --- a/packages/react/src/ActionMenu/ActionMenu.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.tsx @@ -18,7 +18,7 @@ import styles from './ActionMenu.module.css' import {useResponsiveValue, type ResponsiveValue} from '../hooks/useResponsiveValue' import {isSlot} from '../utils/is-slot' import type {FCWithSlotMarker, WithSlotMarker} from '../utils/types/Slots' -import {DialogContext} from '../Dialog/Dialog' +import {DialogContext} from '../Dialog/DialogContext' export type MenuCloseHandler = ( gesture: 'anchor-click' | 'click-outside' | 'escape' | 'tab' | 'item-select' | 'arrow-left' | 'close', diff --git a/packages/react/src/Button/IconButton.tsx b/packages/react/src/Button/IconButton.tsx index 818eb029f9c..b48d54475a5 100644 --- a/packages/react/src/Button/IconButton.tsx +++ b/packages/react/src/Button/IconButton.tsx @@ -2,8 +2,9 @@ import React, {forwardRef, type JSX} from 'react' import type {IconButtonProps} from './types' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' import {ButtonBase} from './ButtonBase' -import {TooltipContext, Tooltip} from '../TooltipV2/Tooltip' -import {TooltipContext as TooltipContextV1} from '../Tooltip/Tooltip' +import {Tooltip} from '../TooltipV2/Tooltip' +import {TooltipContext} from '../TooltipV2/TooltipContext' +import {TooltipContext as TooltipContextV1} from '../Tooltip/TooltipContext' import classes from './ButtonBase.module.css' import {clsx} from 'clsx' diff --git a/packages/react/src/Dialog/Dialog.tsx b/packages/react/src/Dialog/Dialog.tsx index a7634ab1739..39e7cc36960 100644 --- a/packages/react/src/Dialog/Dialog.tsx +++ b/packages/react/src/Dialog/Dialog.tsx @@ -16,6 +16,7 @@ import classes from './Dialog.module.css' import {clsx} from 'clsx' import {useSlots} from '../hooks/useSlots' import {useResizeObserver} from '../hooks/useResizeObserver' +import {DialogContext} from './DialogContext' /* Dialog Version 2 */ @@ -270,8 +271,6 @@ const defaultFooterButtons: Array = [] // Minimum room needed for body content before forcing footer buttons into horizontal scroll. const MIN_BODY_HEIGHT = 48 -// useful to determine whether we're inside a Dialog from a nested component -export const DialogContext = React.createContext(undefined) const DIALOG_CONTEXT_VALUE = Object.freeze({}) const _Dialog = React.forwardRef>((props, forwardedRef) => { diff --git a/packages/react/src/Dialog/DialogContext.ts b/packages/react/src/Dialog/DialogContext.ts new file mode 100644 index 00000000000..ef6b923b19a --- /dev/null +++ b/packages/react/src/Dialog/DialogContext.ts @@ -0,0 +1,3 @@ +import React from 'react' + +export const DialogContext = React.createContext(undefined) diff --git a/packages/react/src/Portal/Portal.features.stories.tsx b/packages/react/src/Portal/Portal.features.stories.tsx index 4a91b0cad4d..1d42d06ba5a 100644 --- a/packages/react/src/Portal/Portal.features.stories.tsx +++ b/packages/react/src/Portal/Portal.features.stories.tsx @@ -1,6 +1,8 @@ import React, {useEffect} from 'react' import type {Meta} from '@storybook/react-vite' -import {Portal, PortalContext, registerPortalRoot} from './Portal' +import {Portal} from './Portal' +import {PortalContext} from './PortalContext' +import {registerPortalRoot} from './portalRoot' import classes from './Portal.stories.module.css' import {clsx} from 'clsx' diff --git a/packages/react/src/Portal/Portal.tsx b/packages/react/src/Portal/Portal.tsx index 98c32142161..b382f895521 100644 --- a/packages/react/src/Portal/Portal.tsx +++ b/packages/react/src/Portal/Portal.tsx @@ -1,55 +1,8 @@ import React, {useContext} from 'react' import {createPortal} from 'react-dom' import useLayoutEffect from '../utils/useIsomorphicLayoutEffect' - -const PRIMER_PORTAL_ROOT_ID = '__primerPortalRoot__' -const DEFAULT_PORTAL_CONTAINER_NAME = '__default__' - -const portalRootRegistry: Partial> = {} - -/** - * Register a container to serve as a portal root. - * @param root The element that will be the root for portals created in this container - * @param name The name of the container, to be used with the `containerName` prop on the Portal Component. - * If name is not specified, registers the default portal root. - */ -export function registerPortalRoot(root: Element, name = DEFAULT_PORTAL_CONTAINER_NAME): void { - portalRootRegistry[name] = root -} - -// Ensures that a default portal root exists and is registered. If a DOM element exists -// with id __primerPortalRoot__, allow that element to serve as the default portal root. -// Otherwise, create that element and attach it to the end of document.body. -function ensureDefaultPortal() { - const existingDefaultPortalContainer = portalRootRegistry[DEFAULT_PORTAL_CONTAINER_NAME] - if (!existingDefaultPortalContainer || !document.body.contains(existingDefaultPortalContainer)) { - let defaultPortalContainer = document.getElementById(PRIMER_PORTAL_ROOT_ID) - if (!(defaultPortalContainer instanceof Element)) { - defaultPortalContainer = document.createElement('div') - defaultPortalContainer.setAttribute('id', PRIMER_PORTAL_ROOT_ID) - defaultPortalContainer.style.position = 'absolute' - defaultPortalContainer.style.top = '0' - defaultPortalContainer.style.left = '0' - defaultPortalContainer.style.width = '100%' - const suitablePortalRoot = document.querySelector('[data-portal-root]') - if (suitablePortalRoot) { - suitablePortalRoot.appendChild(defaultPortalContainer) - } else { - document.body.appendChild(defaultPortalContainer) - } - } - - registerPortalRoot(defaultPortalContainer) - } -} - -/** - * Provides the ability for component trees to override the portal root container for a sub-set of the experience. - * The portal will prioritize the context value unless overridden by their own `containerName` prop, and fallback to the default root if neither are specified - */ -export const PortalContext = React.createContext<{ - portalContainerName?: string -}>({}) +import {PortalContext} from './PortalContext' +import {DEFAULT_PORTAL_CONTAINER_NAME, ensureDefaultPortal, getPortalRoot} from './portalRoot' export interface PortalProps { /** @@ -96,7 +49,7 @@ export const Portal: React.FC> = ({ containerName = DEFAULT_PORTAL_CONTAINER_NAME ensureDefaultPortal() } - const parentElement = portalRootRegistry[containerName] + const parentElement = getPortalRoot(containerName) if (!parentElement) { throw new Error( diff --git a/packages/react/src/Portal/PortalContext.ts b/packages/react/src/Portal/PortalContext.ts new file mode 100644 index 00000000000..9d0fe08dad0 --- /dev/null +++ b/packages/react/src/Portal/PortalContext.ts @@ -0,0 +1,5 @@ +import React from 'react' + +export const PortalContext = React.createContext<{ + portalContainerName?: string +}>({}) diff --git a/packages/react/src/Portal/index.ts b/packages/react/src/Portal/index.ts index 375dce81355..5f64c239f8f 100644 --- a/packages/react/src/Portal/index.ts +++ b/packages/react/src/Portal/index.ts @@ -1,5 +1,7 @@ import type {PortalProps} from './Portal' -import {Portal, registerPortalRoot, PortalContext} from './Portal' +import {Portal} from './Portal' +import {PortalContext} from './PortalContext' +import {registerPortalRoot} from './portalRoot' export default Portal export {registerPortalRoot} diff --git a/packages/react/src/Portal/portalRoot.ts b/packages/react/src/Portal/portalRoot.ts new file mode 100644 index 00000000000..5ef11965c2d --- /dev/null +++ b/packages/react/src/Portal/portalRoot.ts @@ -0,0 +1,41 @@ +const PRIMER_PORTAL_ROOT_ID = '__primerPortalRoot__' +export const DEFAULT_PORTAL_CONTAINER_NAME = '__default__' + +const portalRootRegistry: Partial> = {} + +/** + * Register a container to serve as a portal root. + * @param root The element that will be the root for portals created in this container + * @param name The name of the container, to be used with the `containerName` prop on the Portal Component. + * If name is not specified, registers the default portal root. + */ +export function registerPortalRoot(root: Element, name = DEFAULT_PORTAL_CONTAINER_NAME): void { + portalRootRegistry[name] = root +} + +export function ensureDefaultPortal() { + const existingDefaultPortalContainer = portalRootRegistry[DEFAULT_PORTAL_CONTAINER_NAME] + if (!existingDefaultPortalContainer || !document.body.contains(existingDefaultPortalContainer)) { + let defaultPortalContainer = document.getElementById(PRIMER_PORTAL_ROOT_ID) + if (!(defaultPortalContainer instanceof Element)) { + defaultPortalContainer = document.createElement('div') + defaultPortalContainer.setAttribute('id', PRIMER_PORTAL_ROOT_ID) + defaultPortalContainer.style.position = 'absolute' + defaultPortalContainer.style.top = '0' + defaultPortalContainer.style.left = '0' + defaultPortalContainer.style.width = '100%' + const suitablePortalRoot = document.querySelector('[data-portal-root]') + if (suitablePortalRoot) { + suitablePortalRoot.appendChild(defaultPortalContainer) + } else { + document.body.appendChild(defaultPortalContainer) + } + } + + registerPortalRoot(defaultPortalContainer) + } +} + +export function getPortalRoot(name: string) { + return portalRootRegistry[name] +} diff --git a/packages/react/src/Radio/Radio.tsx b/packages/react/src/Radio/Radio.tsx index 8efa4501af1..aef591a7bde 100644 --- a/packages/react/src/Radio/Radio.tsx +++ b/packages/react/src/Radio/Radio.tsx @@ -1,6 +1,6 @@ import type {ChangeEventHandler, InputHTMLAttributes, ReactElement} from 'react' import React, {useContext} from 'react' -import {RadioGroupContext} from '../RadioGroup/RadioGroup' +import {RadioGroupContext} from '../RadioGroup/RadioGroupContext' import {clsx} from 'clsx' import sharedClasses from '../Checkbox/shared.module.css' import classes from './Radio.module.css' diff --git a/packages/react/src/RadioGroup/RadioGroup.tsx b/packages/react/src/RadioGroup/RadioGroup.tsx index cc9b64b78b0..a86c58eb90e 100644 --- a/packages/react/src/RadioGroup/RadioGroup.tsx +++ b/packages/react/src/RadioGroup/RadioGroup.tsx @@ -1,12 +1,12 @@ import type {ChangeEvent, ChangeEventHandler, FC} from 'react' import type React from 'react' -import {createContext} from 'react' import type {CheckboxOrRadioGroupProps} from '../internal/components/CheckboxOrRadioGroup' import CheckboxOrRadioGroup from '../internal/components/CheckboxOrRadioGroup' import CheckboxOrRadioGroupCaption from '../internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroupCaption' import CheckboxOrRadioGroupLabel from '../internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroupLabel' import CheckboxOrRadioGroupValidation from '../internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroupValidation' import {useRenderForcingRef} from '../hooks' +import {RadioGroupContext} from './RadioGroupContext' export type RadioGroupProps = { /** @@ -19,12 +19,6 @@ export type RadioGroupProps = { name: string } & CheckboxOrRadioGroupProps -export const RadioGroupContext = createContext<{ - disabled?: boolean - onChange?: ChangeEventHandler - name: string -} | null>(null) - const RadioGroup: FC> = ({children, disabled, onChange, name, ...rest}) => { const [selectedRadioValue, setSelectedRadioValue] = useRenderForcingRef(null) diff --git a/packages/react/src/RadioGroup/RadioGroupContext.ts b/packages/react/src/RadioGroup/RadioGroupContext.ts new file mode 100644 index 00000000000..91329a4ee0f --- /dev/null +++ b/packages/react/src/RadioGroup/RadioGroupContext.ts @@ -0,0 +1,7 @@ +import {type ChangeEventHandler, createContext} from 'react' + +export const RadioGroupContext = createContext<{ + disabled?: boolean + onChange?: ChangeEventHandler + name: string +} | null>(null) diff --git a/packages/react/src/RadioGroup/index.ts b/packages/react/src/RadioGroup/index.ts index 7c2be01c513..c96f2e727b4 100644 --- a/packages/react/src/RadioGroup/index.ts +++ b/packages/react/src/RadioGroup/index.ts @@ -1,3 +1,4 @@ -export {default, RadioGroupContext} from './RadioGroup' +export {default} from './RadioGroup' +export {RadioGroupContext} from './RadioGroupContext' export type {RadioGroupProps} from './RadioGroup' diff --git a/packages/react/src/Tooltip/Tooltip.tsx b/packages/react/src/Tooltip/Tooltip.tsx index 230ee4a3216..0869b055383 100644 --- a/packages/react/src/Tooltip/Tooltip.tsx +++ b/packages/react/src/Tooltip/Tooltip.tsx @@ -3,6 +3,7 @@ import React, {useMemo} from 'react' import {useId} from '../hooks' import classes from './Tooltip.module.css' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' +import {TooltipContext} from './TooltipContext' /* Tooltip v1 */ @@ -17,8 +18,6 @@ export type TooltipProps = { wrap?: boolean } & React.ComponentProps<'span'> -export const TooltipContext = React.createContext<{tooltipId?: string}>({}) - /** * @deprecated */ diff --git a/packages/react/src/Tooltip/TooltipContext.ts b/packages/react/src/Tooltip/TooltipContext.ts new file mode 100644 index 00000000000..a443a86ac6a --- /dev/null +++ b/packages/react/src/Tooltip/TooltipContext.ts @@ -0,0 +1,3 @@ +import React from 'react' + +export const TooltipContext = React.createContext<{tooltipId?: string}>({}) diff --git a/packages/react/src/TooltipV2/Tooltip.tsx b/packages/react/src/TooltipV2/Tooltip.tsx index cb264360c98..42e0bb47242 100644 --- a/packages/react/src/TooltipV2/Tooltip.tsx +++ b/packages/react/src/TooltipV2/Tooltip.tsx @@ -12,6 +12,7 @@ import {usePlatform} from '../KeybindingHint/platform' import VisuallyHidden from '../_VisuallyHidden' import useSafeTimeout from '../hooks/useSafeTimeout' import type {SlotMarker} from '../utils/types' +import {TooltipContext} from './TooltipContext' export type TooltipDirection = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' export type TooltipProps = React.PropsWithChildren<{ @@ -102,8 +103,6 @@ const isInteractive = (element: HTMLElement) => { (element.hasAttribute('role') && element.getAttribute('role') === 'button') ) } -export const TooltipContext = React.createContext<{tooltipId?: string}>({}) - const emptyKeybindingHints: Array = [] export const Tooltip: ForwardRefExoticComponent< diff --git a/packages/react/src/TooltipV2/TooltipContext.ts b/packages/react/src/TooltipV2/TooltipContext.ts new file mode 100644 index 00000000000..a443a86ac6a --- /dev/null +++ b/packages/react/src/TooltipV2/TooltipContext.ts @@ -0,0 +1,3 @@ +import React from 'react' + +export const TooltipContext = React.createContext<{tooltipId?: string}>({})