1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Introduce Element pattern to reduce duplication in UI code

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen 2023-04-12 10:02:16 +03:00
parent 3e150adabc
commit 99b0c8639f
No known key found for this signature in database
GPG Key ID: 8C6CFB2FFFE8F68A
11 changed files with 250 additions and 1 deletions

View File

@ -1 +1,3 @@
export { uiComponentsFeature } from "./src/feature";
export * from "./src/element/elements";

View File

@ -30,12 +30,16 @@
"lint": "lens-lint",
"lint:fix": "lens-lint --fix"
},
"dependencies": {
"classnames": "^2.3.2"
},
"peerDependencies": {
"@k8slens/feature-core": "^6.5.0-alpha.0",
"@ogre-tools/injectable": "^15.1.2",
"@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2",
"@ogre-tools/fp": "^15.1.2",
"lodash": "^4.17.21"
"lodash": "^4.17.21",
"react": "^17"
},
"devDependencies": {
"@async-fn/jest": "^1.6.4",

View File

@ -0,0 +1,9 @@
import type React from "react";
export {};
declare global {
interface ElementProps {
children?: React.ReactNode;
}
}

View File

@ -0,0 +1,18 @@
import React, { HTMLAttributes } from "react";
import { pipeline } from "@ogre-tools/fp";
import { flexParentModification } from "./prop-modifications/flex/flex-parent";
import { classNameModification } from "./prop-modifications/class-names/class-names";
import { vanillaClassNameAdapterModification } from "./prop-modifications/class-names/vanilla-class-name-adapter";
export const ElementFor =
<T extends HTMLElement, Y extends HTMLAttributes<T>>(TagName: React.ElementType) =>
(props: React.DetailedHTMLProps<Y, T> & ElementProps) => {
const modifiedProps = pipeline(
props,
vanillaClassNameAdapterModification,
flexParentModification,
classNameModification,
);
return <TagName {...modifiedProps} />;
};

View File

@ -0,0 +1,5 @@
import { ElementFor } from "./element";
import type React from "react";
export const Div = ElementFor<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>("div");
export const Span = ElementFor<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>("span");

View File

@ -0,0 +1,56 @@
import { Div } from "../../elements";
import { render } from "@testing-library/react";
import React from "react";
import { discoverFor } from "@k8slens/react-testing-library-discovery";
describe("class-names", () => {
it("given complex class names, renders with class name", () => {
const rendered = render(
<Div
_className={[
"some-class-name",
{
"some-not-present-class-name": false,
"some-present-class-name": true,
},
["first-class-name-in-array", "second-class-name-in-array"],
undefined,
false,
]}
data-some-element-test
/>,
);
const discover = discoverFor(() => rendered);
const { discovered } = discover.getSingleElement("some-element");
expect(discovered.className).toBe(
"some-class-name some-present-class-name first-class-name-in-array second-class-name-in-array",
);
});
it("given minimal class name, renders with class name", () => {
const rendered = render(<Div _className="some-class-name" data-some-element-test />);
const discover = discoverFor(() => rendered);
const { discovered } = discover.getSingleElement("some-element");
expect(discovered.className).toBe("some-class-name");
});
it("given complex class names leading to class name not being present, renders without class name", () => {
const rendered = render(
<Div _className={[{ some: false }, [undefined, false]]} data-some-element-test />,
);
const discover = discoverFor(() => rendered);
const { discovered } = discover.getSingleElement("some-element");
expect(discovered).not.toHaveAttribute('class');
});
});

View File

@ -0,0 +1,18 @@
import classnames from "classnames";
export type ClassName = classnames.Argument;
declare global {
interface ElementProps {
_className?: ClassName;
}
}
export const classNameModification = <T extends ElementProps>({ _className, ...props }: T) => {
const classNameString = classnames(_className);
return {
...props,
...(classNameString ? { className: classNameString } : {}),
};
};

View File

@ -0,0 +1,42 @@
import { Div } from "../../elements";
import { render } from "@testing-library/react";
import React from "react";
import { discoverFor } from "@k8slens/react-testing-library-discovery";
describe("vanilla-class-name-adapter", () => {
it("given vanilla class name, has class name", () => {
const rendered = render(<Div className="some-class-name" data-some-element-test />);
const discover = discoverFor(() => rendered);
const { discovered } = discover.getSingleElement("some-element");
expect(discovered.className).toBe("some-class-name");
});
it("given custom class name, has class name", () => {
const rendered = render(<Div _className="some-class-name" data-some-element-test />);
const discover = discoverFor(() => rendered);
const { discovered } = discover.getSingleElement("some-element");
expect(discovered.className).toBe("some-class-name");
});
it("given both vanilla and custom class names, has class names", () => {
const rendered = render(
<Div
className="some-vanilla-class-name"
_className="some-class-name"
data-some-element-test
/>,
);
const discover = discoverFor(() => rendered);
const { discovered } = discover.getSingleElement("some-element");
expect(discovered.className).toBe("some-vanilla-class-name some-class-name");
});
});

View File

@ -0,0 +1,10 @@
import type { ClassName } from "./class-names";
export const vanillaClassNameAdapterModification = <T extends ElementProps>({
className,
_className,
...props
}: T & { className?: string; _className?: ClassName }) => ({
...props,
_className: [className, _className],
});

View File

@ -0,0 +1,50 @@
import { Div } from "../../elements";
import { render } from "@testing-library/react";
import React from "react";
import { discoverFor } from "@k8slens/react-testing-library-discovery";
describe("flexParent", () => {
it("given flex parent without specific configuration, renders", () => {
const rendered = render(<Div _flexParent data-some-element-test />);
const discover = discoverFor(() => rendered);
const { discovered } = discover.getSingleElement("some-element");
expect(discovered.className).toBe("flex");
});
it("given flex parent prop with false, renders", () => {
const rendered = render(<Div _flexParent={false} data-some-element-test />);
const discover = discoverFor(() => rendered);
const { discovered } = discover.getSingleElement("some-element");
expect(discovered.className).toBe("");
});
it("given flex parent with aligning child vertically center, renders", () => {
const rendered = render(
<Div _flexParent={{ centeredVertically: true }} data-some-element-test />,
);
const discover = discoverFor(() => rendered);
const { discovered } = discover.getSingleElement("some-element");
expect(discovered.className).toBe("flex align-center");
});
it("given flex parent with explicitly not aligning child vertically center, renders", () => {
const rendered = render(
<Div _flexParent={{ centeredVertically: false }} data-some-element-test />,
);
const discover = discoverFor(() => rendered);
const { discovered } = discover.getSingleElement("some-element");
expect(discovered.className).toBe("flex");
});
});

View File

@ -0,0 +1,35 @@
import type { ClassName } from "../class-names/class-names";
declare global {
interface ElementProps {
_className?: ClassName;
_flexParent?: { centeredVertically: boolean } | boolean;
}
}
export const flexParentModification = <T extends ElementProps>({
_flexParent,
_className,
...props
}: T) => {
if (!_flexParent) {
return { _className, ...props };
}
const centeredVertically =
typeof _flexParent === "boolean" ? false : _flexParent.centeredVertically;
return {
...props,
_className: [
"flex",
{
"align-center": centeredVertically,
},
_className,
],
};
};