Skip to content

Commit

Permalink
feat(atomic-elements): Add useSlottedContext for slots support. Updat…
Browse files Browse the repository at this point in the history
…e useContextProps to use useSlottedContext instead of useContext directly
  • Loading branch information
seanrcollings committed Nov 21, 2024
1 parent 19e6aed commit 04bb2de
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 12 deletions.
16 changes: 10 additions & 6 deletions packages/atomic-elements/src/hooks/useContextProps.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useContext } from "react";
import { mergeProps, mergeRefs, useObjectRef } from "@react-aria/utils";
import { SlotProps, SlottedContextValue, useSlottedContext } from './useSlottedContext';

type Props = Record<string, any>;
type PropsArg = Props | null | undefined;
type WithRef<T, R> = T & { ref?: React.Ref<R> };
export type ContextValue<T, R> = SlottedContextValue<WithRef<T, R>>;

export function useContextProps<T extends PropsArg>(
export function useContextProps<T extends {}>(
context: React.Context<T>,
props: T
) {
Expand All @@ -14,12 +14,16 @@ export function useContextProps<T extends PropsArg>(
return mergeProps(contextProps, props);
}

export function useContextPropsV2<T extends PropsArg, R>(
context: React.Context<WithRef<Partial<T>, R>>,

// from: https://github.com/adobe/react-spectrum/blob/c6bd2cb0808838a9f1f850b6c1ffe88465254222/packages/react-aria-components/src/utils.tsx#L180
/** Consume a value from a context & merge it with the provided props */
export function useContextPropsV2<T extends object, R>(
context: React.Context<ContextValue<T, R>>,
props: T,
ref: React.Ref<R>
): [T, React.RefObject<R>] {
const { ref: contextRef, ...contextProps } = useContext(context);
const ctx = useSlottedContext(context, (props as SlotProps).slot) || {}
const { ref: contextRef, ...contextProps } = ctx as WithRef<T, R>;

const mergedRef = useObjectRef(mergeRefs(ref, contextRef!));

Expand Down
44 changes: 44 additions & 0 deletions packages/atomic-elements/src/hooks/useSlottedContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useContext } from 'react';

export type SlottedContextValue<T> = T | {
slots?: Record<string | symbol, T>
} | null | undefined;

export interface SlotProps {
slot?: string | null;
}

export const DEFAULT_SLOT = Symbol("DEFAULT_SLOT");

// from: https://github.com/adobe/react-spectrum/blob/c6bd2cb0808838a9f1f850b6c1ffe88465254222/packages/react-aria-components/src/utils.tsx#L157
/** Consume a context OR consume a slotted context value */
export function useSlottedContext<T>(
context: React.Context<SlottedContextValue<T>>,
slot?: string | null
): T | null | undefined {
const ctx = useContext(context);
if (slot === null) {
// An explicit `null` slot means don't use context.
return null;
}

// If slots are provided by the context we need to consume the correct slot
if (ctx && typeof ctx === "object" && "slots" in ctx && ctx.slots) {
const allSlots = Object.keys(ctx.slots);

if (!slot && !ctx.slots[DEFAULT_SLOT]) {
throw new Error(`A slot prop is required. Valid slot names are ${allSlots.join(", ")}`);
}

const key = slot || DEFAULT_SLOT;

if (!ctx.slots[key]) {
throw new Error(`Invalid slot name. Valid slot names are ${allSlots.join(", ")}`);
}

return ctx.slots[key];
}

// The context doesn't provide a slot, consume the context as is
return ctx as T;
}
11 changes: 5 additions & 6 deletions packages/atomic-elements/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { SlottedContextValue } from "@hooks/useSlottedContext";
import React from "react";

export function createComponentContext<T extends object>(
defaultValue: Partial<T> = {}
) {
return React.createContext<Partial<T & { ref?: React.Ref<any> }>>(
defaultValue
);
export function createComponentContext<T>() {
return React.createContext<SlottedContextValue<T>>(null as any);
}


0 comments on commit 04bb2de

Please sign in to comment.