Skip to content

Commit

Permalink
fix: component identification with display name
Browse files Browse the repository at this point in the history
  • Loading branch information
abelflopes committed Jul 5, 2024
1 parent b5b5031 commit b182afb
Show file tree
Hide file tree
Showing 6 changed files with 44 additions and 12 deletions.
1 change: 1 addition & 0 deletions packages/components/button/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const Button = <T extends HTMLTag>({
// Validate icon usage (icon should be set through specific prop)
for (const i of React.Children.toArray(children).filter(isValidElement)) {
const name = getDisplayName(i);
// use any icon display name instead of DISPLAY_NAMES const to have more coverage
if (name && name.toLowerCase().includes("icon"))
throw new Error("Icons inside Button should be set with 'icon' prop");
}
Expand Down
1 change: 1 addition & 0 deletions packages/components/icon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
},
"dependencies": {
"@react-ck/theme": "^1.8.0",
"@react-ck/react-utils": "^1.3.0",
"classnames": "^2.3.2",
"react-icons": "^4.10.1"
}
Expand Down
9 changes: 7 additions & 2 deletions packages/components/icon/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import styles from "./styles/index.module.scss";
import React from "react";
import { useThemeContext } from "@react-ck/theme";
import classNames from "classnames";
import { DISPLAY_NAME_ATTRIBUTE, DISPLAY_NAMES } from "@react-ck/react-utils";

export interface IconProps extends React.HTMLAttributes<HTMLSpanElement> {
interface IconProps extends React.HTMLAttributes<HTMLSpanElement> {
size?: "text" | "m" | "l";
skin?: "default" | "inverted";
children: NonNullable<React.ReactNode>;
Expand All @@ -15,7 +16,7 @@ export interface IconProps extends React.HTMLAttributes<HTMLSpanElement> {
* @returns a React element
*/

export const Icon = ({
const Icon = ({
size = "text",
skin = "default",
className,
Expand All @@ -40,3 +41,7 @@ export const Icon = ({
</span>
);
};

Icon[DISPLAY_NAME_ATTRIBUTE] = DISPLAY_NAMES.ICON;

export { Icon, type IconProps };
8 changes: 6 additions & 2 deletions packages/components/select/src/SelectOption.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Menu } from "@react-ck/provisional";
import React, { useContext, useMemo } from "react";
import { SelectContext } from "./context";
import { componentToText } from "@react-ck/react-utils";
import { componentToText, DISPLAY_NAME_ATTRIBUTE } from "@react-ck/react-utils";
import { type SelectOptionProps } from "./types";

export const SelectOption = ({
const SelectOption = ({
value,
disabled,
children,
Expand Down Expand Up @@ -40,3 +40,7 @@ export const SelectOption = ({
</Menu.Item>
);
};

SelectOption[DISPLAY_NAME_ATTRIBUTE] = "SelectOption";

export { SelectOption };
13 changes: 8 additions & 5 deletions packages/providers/layers/src/Layer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import React, { isValidElement, useContext, useEffect, useMemo } from "react";
import { createPortal } from "react-dom";
import { LayersContext } from "./context";
import { type Elevation } from "@react-ck/elevation";
import { getDisplayName } from "@react-ck/react-utils";
import { DISPLAY_NAME_ATTRIBUTE, getDisplayName, DISPLAY_NAMES } from "@react-ck/react-utils";
import { ThemeProvider, useThemeContext } from "@react-ck/theme";

export interface LayerProps {
interface LayerProps {
/** The elevation level for the layer */
elevation: Elevation;
/** The child components to be rendered within the layer */
Expand All @@ -21,7 +21,7 @@ export interface LayerProps {
* @returns The rendered content of the layer
*/

export const Layer = ({ elevation, children }: Readonly<LayerProps>): React.ReactNode => {
const Layer = ({ elevation, children }: Readonly<LayerProps>): React.ReactNode => {
const theme = useThemeContext();
const { createLayer, usePortal } = useContext(LayersContext);

Expand Down Expand Up @@ -49,10 +49,13 @@ export const Layer = ({ elevation, children }: Readonly<LayerProps>): React.Reac
// Validate icon usage (icon should be set through specific prop)
for (const i of React.Children.toArray(children).filter(isValidElement)) {
const name = getDisplayName(i);
if (name && name.toLowerCase().includes("layer"))
throw new Error("Layers should not be directly nested");
if (name === DISPLAY_NAMES.LAYER) throw new Error("Layers should not be directly nested");
}
}, [children]);

return undefined;
};

Layer[DISPLAY_NAME_ATTRIBUTE] = DISPLAY_NAMES.LAYER;

export { Layer, type LayerProps };
24 changes: 21 additions & 3 deletions packages/utils/react/src/display-name.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
import React from "react";

export const DISPLAY_NAME_ATTRIBUTE = "_react-ck-display-name";

export const DISPLAY_NAMES = {
SELECT_OPTION: "select-option",
LAYER: "layer",
ICON: "icon",
};

export function getDisplayName(el: React.ReactNode): string | undefined {
if (!React.isValidElement(el)) return undefined;
else if (typeof el.type === "string") return el.type;
else if (typeof el.type !== "string" && "name" in el.type) return el.type.name;
return undefined;

let displayName: string | undefined = undefined;

if (typeof el.type === "string") {
displayName = el.type;
} else if (typeof el.type === "function") {
const functionProperties = Object.fromEntries(Object.entries(el.type));
const r: unknown = functionProperties[DISPLAY_NAME_ATTRIBUTE];
if (typeof r !== "string" && r !== undefined) throw new Error("Unknown component name type");
displayName = r;
}

return displayName;
}

0 comments on commit b182afb

Please sign in to comment.