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:
parent
3e150adabc
commit
99b0c8639f
@ -1 +1,3 @@
|
||||
export { uiComponentsFeature } from "./src/feature";
|
||||
|
||||
export * from "./src/element/elements";
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import type React from "react";
|
||||
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
interface ElementProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
}
|
||||
@ -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} />;
|
||||
};
|
||||
@ -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");
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
@ -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 } : {}),
|
||||
};
|
||||
};
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
@ -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],
|
||||
});
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
],
|
||||
};
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user