Skip to content

[♻️]: Refactor to unify render contexts and code styles related to it #51

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions lib/Breadcrumb/Breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import * as React from "react";
import { getLabelInfo, logger } from "../internals";
import type { Classes, MergeElementProps } from "../types";
import { componentWithForwardedRef, useDeterministicId } from "../utils";
import {
componentWithForwardedRef,
isFragment,
useDeterministicId,
} from "../utils";
import { Item, SeparatorItem, type ItemProps } from "./components";
import {
Label as LabelSlot,
Expand Down Expand Up @@ -84,14 +88,21 @@ const BreadcrumbBase = (props: Props, ref: React.Ref<HTMLElement>) => {
const labelProps = getLabelInfo(label, "Breadcrumb");

const children = React.Children.map(childrenProp, child => {
if (!React.isValidElement(child)) return null;
if (!React.isValidElement(child) || isFragment(child)) {
logger(
"The <Breadcrumb.Root> component doesn't accept `Fragment` or any invalid element as children.",
{ scope: "Breadcrumb", type: "error" },
);

return null;
}

if (
(child as React.ReactElement).type !== Item &&
(child as React.ReactElement).type !== SeparatorItem
) {
logger(
"The Breadcrumb component only accepts <Breadcrumb.Item> and " +
"The <Breadcrumb.Root> component only accepts <Breadcrumb.Item> and " +
"<Breadcrumb.Separator> as a children",
{ scope: "Breadcrumb", type: "error" },
);
Expand Down
36 changes: 23 additions & 13 deletions lib/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from "react";
import { logger } from "../internals";
import type { PolymorphicProps } from "../types";
import type { PolymorphicProps, PropWithRenderContext } from "../types";
import {
componentWithForwardedRef,
computeAccessibleName,
Expand All @@ -11,22 +11,28 @@ import {
} from "../utils";
import * as Slots from "./slots";

export type RenderProps = {
/**
* The `disabled` state of the component.
*/
disabled: boolean;
/**
* Determines whether it is focused-visible or not.
*/
focusedVisible: boolean;
};

export type ClassNameProps = RenderProps;

type OwnProps = {
/**
* The content of the component.
*/
children?:
| React.ReactNode
| ((ctx: {
disabled: boolean;
focusedVisible: boolean;
}) => React.ReactNode);
children?: PropWithRenderContext<React.ReactNode, RenderProps>;
/**
* The className applied to the component.
*/
className?:
| string
| ((ctx: { disabled: boolean; focusedVisible: boolean }) => string);
className?: PropWithRenderContext<string, ClassNameProps>;
/**
* If `true`, the component will be disabled.
* @default false
Expand Down Expand Up @@ -70,11 +76,13 @@ const ButtonBase = <E extends React.ElementType, R extends HTMLElement>(
const rootRef = React.useRef<HTMLElement>(null);
const handleRef = useForkedRefs(ref, rootRef, buttonBase.handleButtonRef);

const renderCtx = {
const renderProps: RenderProps = {
disabled,
focusedVisible: buttonBase.isFocusedVisible,
};

const classNameProps = renderProps;

const refCallback = (node: HTMLElement | null) => {
handleRef(node);

Expand Down Expand Up @@ -110,11 +118,13 @@ const ButtonBase = <E extends React.ElementType, R extends HTMLElement>(

const className =
typeof classNameProp === "function"
? classNameProp(renderCtx)
? classNameProp(classNameProps)
: classNameProp;

const children =
typeof childrenProp === "function" ? childrenProp(renderCtx) : childrenProp;
typeof childrenProp === "function"
? childrenProp(renderProps)
: childrenProp;

return (
<RootNode
Expand Down
7 changes: 6 additions & 1 deletion lib/Button/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
export { default, type Props as ButtonProps } from "./Button";
export {
default,
type ClassNameProps as ButtonClassNameProps,
type Props as ButtonProps,
type RenderProps as ButtonRenderProps,
} from "./Button";
29 changes: 19 additions & 10 deletions lib/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from "react";
import { CheckGroupContext } from "../CheckGroup/context";
import { SystemError, getLabelInfo, logger } from "../internals";
import type { Classes, MergeElementProps } from "../types";
import type { ClassesWithRenderContext, MergeElementProps } from "../types";
import {
componentWithForwardedRef,
useCheckBase,
Expand All @@ -12,24 +12,33 @@ import {
import { CheckIcon, IndeterminateIcon } from "./components";
import * as Slots from "./slots";

type CheckboxClassesMap = Classes<"root" | "label" | "check">;

type ClassesContext = {
/** The `checked` state of the checkbox. */
export type ClassNameProps = {
/**
* The `checked` state of the checkbox.
*/
checked: boolean;
/** The `disabled` state of the checkbox. */
/**
* The `disabled` state of the checkbox.
*/
disabled: boolean;
/** The `indeterminated` state of the checkbox. */
/**
* The `indeterminated` state of the checkbox.
*/
indeterminated: boolean;
/** The `:focus-visible` of the checkbox. */
/**
* The `:focus-visible` of the checkbox.
*/
focusedVisible: boolean;
};

type OwnProps = {
/**
* Map of sub-components and their correlated classNames.
*/
classes?: ((ctx: ClassesContext) => CheckboxClassesMap) | CheckboxClassesMap;
classes?: ClassesWithRenderContext<
"root" | "label" | "check",
ClassNameProps
>;
/**
* The label of the checkbox.
*/
Expand Down Expand Up @@ -152,7 +161,7 @@ const CheckboxBase = (props: Props, ref: React.Ref<HTMLButtonElement>) => {

const labelProps = getLabelInfo(label, "Checkbox");

const classesCtx: ClassesContext = {
const classesCtx: ClassNameProps = {
disabled,
indeterminated,
checked: checkBase.checked,
Expand Down
6 changes: 5 additions & 1 deletion lib/Checkbox/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export { default, type Props as CheckboxProps } from "./Checkbox";
export {
default,
type ClassNameProps as CheckboxClassNameProps,
type Props as CheckboxProps,
} from "./Checkbox";
19 changes: 13 additions & 6 deletions lib/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from "react";
import Portal from "../Portal";
import type { Classes, MergeElementProps } from "../types";
import type { ClassesWithRenderContext, MergeElementProps } from "../types";
import {
componentWithForwardedRef,
useDeterministicId,
Expand All @@ -12,7 +12,12 @@ import {
import { DialogContext, type DialogContextValue } from "./context";
import { Backdrop as BackdropSlot, Root as RootSlot } from "./slots";

type DialogClassesMap = Classes<"root" | "backdrop">;
export type ClassNameProps = {
/**
* The `open` state of the component.
*/
openState: boolean;
};

type OwnProps = {
/**
Expand All @@ -22,9 +27,7 @@ type OwnProps = {
/**
* Map of sub-components and their correlated classNames.
*/
classes?:
| DialogClassesMap
| ((ctx: { openState: boolean }) => DialogClassesMap);
classes?: ClassesWithRenderContext<"root" | "backdrop", ClassNameProps>;
/**
* If `true`, the dialog will be opened.
*/
Expand Down Expand Up @@ -112,9 +115,13 @@ const DialogBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
else enablePageScroll();
});

const classNameProps: ClassNameProps = {
openState: open,
};

const classes =
typeof classesProp === "function"
? classesProp({ openState: open })
? classesProp(classNameProps)
: classesProp;

const context = React.useMemo<DialogContextValue>(
Expand Down
16 changes: 13 additions & 3 deletions lib/Dialog/components/Content.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from "react";
import { logger } from "../../internals";
import FocusTrap from "../../internals/FocusTrap";
import type { MergeElementProps } from "../../types";
import { componentWithForwardedRef, useDeterministicId } from "../../utils";
Expand All @@ -24,18 +25,27 @@ export type Props = Omit<
const ContentBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
const { className, children, id: idProp, ...otherProps } = props;

const dialogCtx = React.useContext(DialogContext);
const ctx = React.useContext(DialogContext);

const id = useDeterministicId(idProp, "styleless-ui__dialog-content");

if (!ctx) {
logger("You have to use this component as a descendant of <Dialog.Root>.", {
scope: "Dialog.Content",
type: "error",
});

return null;
}

return (
<FocusTrap enabled={dialogCtx?.open}>
<FocusTrap enabled={ctx.open}>
<div
{...otherProps}
id={id}
ref={ref}
className={className}
role={dialogCtx?.role}
role={ctx.role}
data-slot={ContentRootSlot}
aria-modal="true"
>
Expand Down
6 changes: 5 additions & 1 deletion lib/Dialog/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export { default as Root, type Props as RootProps } from "./Dialog";
export {
default as Root,
type ClassNameProps as RootClassNameProps,
type Props as RootProps,
} from "./Dialog";
export * from "./components";
27 changes: 20 additions & 7 deletions lib/Expandable/Expandable.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import * as React from "react";
import type { MergeElementProps } from "../types";
import type { MergeElementProps, PropWithRenderContext } from "../types";
import { componentWithForwardedRef, useControlledProp } from "../utils";
import { ExpandableContext } from "./context";
import { Root as RootSlot } from "./slots";

export type RenderProps = {
/**
* Determines whether it is expanded or not.
*/
expanded: boolean;
};

export type ClassNameProps = RenderProps;

type OwnProps = {
/**
* The content of the component.
*/
children?:
| React.ReactNode
| ((ctx: { expanded: boolean }) => React.ReactNode);
children?: PropWithRenderContext<React.ReactNode, RenderProps>;
/**
* The className applied to the component.
*/
className?: string | ((ctx: { expanded: boolean }) => string);
className?: PropWithRenderContext<string, ClassNameProps>;
/**
* If `true`, the panel will be opened.
*/
Expand Down Expand Up @@ -52,14 +59,20 @@ const ExpandableBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
false,
);

const renderProps: RenderProps = {
expanded: isExpanded,
};

const classNameProps = renderProps;

const className =
typeof classNameProp === "function"
? classNameProp({ expanded: isExpanded })
? classNameProp(classNameProps)
: classNameProp;

const children =
typeof childrenProp === "function"
? childrenProp({ expanded: isExpanded })
? childrenProp(renderProps)
: childrenProp;

const handleExpandChange = (expandState: boolean) => {
Expand Down
21 changes: 17 additions & 4 deletions lib/Expandable/components/Content.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from "react";
import { logger } from "../../internals";
import type { MergeElementProps } from "../../types";
import {
componentWithForwardedRef,
Expand Down Expand Up @@ -31,10 +32,22 @@ export type Props = Omit<
const ContentBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
const { children, className, id: idProp, ...otherProps } = props;

const expandableCtx = React.useContext(ExpandableContext);
const ctx = React.useContext(ExpandableContext);

const id = useDeterministicId(idProp, "styleless-ui__expandable-content");

if (!ctx) {
logger(
"You have to use this component as a descendant of <Expandable.Root>.",
{
scope: "Expandable.Content",
type: "error",
},
);

return null;
}

const refCallback = (node: HTMLDivElement | null) => {
setRef(ref, node);

Expand Down Expand Up @@ -62,11 +75,11 @@ const ContentBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
ref={refCallback}
className={className}
role="region"
aria-hidden={!expandableCtx?.isExpanded}
aria-hidden={!ctx.isExpanded}
// @ts-expect-error React hasn't added `inert` yet
inert={!expandableCtx?.isExpanded ? undefined : ""}
inert={!ctx.isExpanded ? undefined : ""}
data-slot={ContentRootSlot}
data-expanded={!expandableCtx?.isExpanded ? "" : undefined}
data-expanded={!ctx.isExpanded ? "" : undefined}
>
{children}
</div>
Expand Down
Loading