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

Introduce helper component to render list of React elements

Features:
- Placeholder for empty list
- Separators between items
- No boilerplate for "key" prop in React

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 2022-08-17 14:09:15 +03:00
parent 43e0c49f06
commit 76991b59e6
No known key found for this signature in database
GPG Key ID: 8C6CFB2FFFE8F68A
5 changed files with 316 additions and 0 deletions

View File

@ -0,0 +1,58 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Map given items and placeholder but no separator renders 1`] = `
<body>
<div>
<div
data-testid="some-item-id"
/>
<div
data-testid="some-other-item-id"
/>
</div>
</body>
`;
exports[`Map given more than one item and separator renders 1`] = `
<body>
<div>
<div
data-testid="some-item-id"
/>
<div
data-testid="separator"
>
Some separator
</div>
<div
data-testid="some-other-item-id"
/>
<div
data-testid="separator"
>
Some separator
</div>
<div
data-testid="some-another-item-id"
/>
</div>
</body>
`;
exports[`Map given no items and placeholder renders 1`] = `
<body>
<div>
<div
data-testid="some-placeholder"
>
Some placeholder
</div>
</div>
</body>
`;
exports[`Map given no items but no placeholder renders 1`] = `
<body>
<div />
</body>
`;

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { addSeparator } from "./add-separator";
describe("add-separator", () => {
it("given multiple items, adds separators", () => {
const items = ["first", "second", "third"];
const actual = addSeparator((left, right) => `separator-between-${left}-and-${right}`, items);
expect(actual).toEqual([
"first",
"separator-between-first-and-second",
"second",
"separator-between-second-and-third",
"third",
]);
});
it("given multiple items including falsy ones, adds separators", () => {
const items = [false, undefined, null, NaN];
const actual = addSeparator((left, right) => `separator-between-${left}-and-${right}`, items);
expect(actual).toEqual([
false,
"separator-between-false-and-undefined",
undefined,
"separator-between-undefined-and-null",
null,
"separator-between-null-and-NaN",
NaN,
]);
});
it("given no items, does not add separator", () => {
const items: any[] = [];
const actual = addSeparator(() => "separator", items);
expect(actual).toEqual([]);
});
it("given one item, does not add separator", () => {
const items = ["first"];
const actual = addSeparator(() => "separator", items);
expect(actual).toEqual(["first"]);
});
});

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
type GetSeparator<Item, Separator> = (left: Item, right: Item) => Separator;
export const addSeparator = <Item, Separator>(
getSeparator: GetSeparator<Item, Separator>,
items: Item[],
) => items.flatMap(toSeparatedTupleUsing(getSeparator));
const toSeparatedTupleUsing =
<Item, Separator>(getSeparator: GetSeparator<Item, Separator>) =>
(leftItem: Item, index: number, arr: Item[]) => {
const itemIsLast = arr.length === index + 1;
if (itemIsLast) {
return [leftItem];
}
const rightItem = arr[index + 1];
const separator = getSeparator(leftItem, rightItem);
return [leftItem, separator];
};

View File

@ -0,0 +1,121 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
import { render } from "@testing-library/react";
import React from "react";
import { Map } from "./map";
describe("Map", () => {
describe("given no items and placeholder", () => {
let rendered: RenderResult;
beforeEach(() => {
rendered = render(
<Map
items={[]}
getPlaceholder={() => (
<div data-testid="some-placeholder">Some placeholder</div>
)}
>
{() => <div data-testid="some-row">Irrelevant</div>}
</Map>,
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("renders placeholder", () => {
expect(rendered.getByTestId("some-placeholder")).toBeInTheDocument();
});
it("does not render rows", () => {
expect(rendered.queryByTestId("some-row")).not.toBeInTheDocument();
});
});
describe("given no items but no placeholder", () => {
let rendered: RenderResult;
beforeEach(() => {
rendered = render(
<Map items={[]}>
{() => <div data-testid="some-row">Irrelevant</div>}
</Map>,
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not render rows", () => {
expect(rendered.queryByTestId("some-row")).not.toBeInTheDocument();
});
});
describe("given items and placeholder but no separator", () => {
let rendered: RenderResult;
beforeEach(() => {
rendered = render(
<Map
items={[{ id: "some-item-id" }, { id: "some-other-item-id" }]}
getPlaceholder={() => (
<div data-testid="some-placeholder">Some placeholder</div>
)}
>
{(item) => <div data-testid={item.id} />}
</Map>,
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not render placeholder", () => {
expect(
rendered.queryByTestId("some-placeholder"),
).not.toBeInTheDocument();
});
it("renders items", () => {
expect(rendered.getByTestId("some-item-id")).toBeInTheDocument();
});
});
describe("given more than one item and separator", () => {
let rendered: RenderResult;
beforeEach(() => {
rendered = render(
<Map
items={[
{ id: "some-item-id" },
{ id: "some-other-item-id" },
{ id: "some-another-item-id" },
]}
getSeparator={() => <div data-testid="separator">Some separator</div>}
>
{(item) => <div data-testid={item.id} />}
</Map>,
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("renders items", () => {
expect(rendered.getByTestId("some-item-id")).toBeInTheDocument();
});
it("renders separator", () => {
expect(rendered.getAllByTestId("separator")).toHaveLength(2);
});
});
});

View File

@ -0,0 +1,58 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { pipeline } from "@ogre-tools/fp";
import { identity, map } from "lodash/fp";
import React from "react";
import { addSeparator } from "./add-separator/add-separator";
interface RequiredPropertiesForItem {
id: string;
}
interface MapProps<Item extends RequiredPropertiesForItem> {
items: Item[];
children: (item: Item) => React.ReactElement;
getPlaceholder?: () => React.ReactElement;
getSeparator?: () => React.ReactElement;
}
export const Map = <Item extends RequiredPropertiesForItem>(
props: MapProps<Item>,
) => {
const { items, getPlaceholder, getSeparator, children } = props;
if (items.length === 0) {
return getPlaceholder?.() || null;
}
return (
<>
{pipeline(
items,
map((item) => ({ item, render: () => children(item) })),
getSeparator
? (items) =>
addSeparator(
(left, right) => ({
item: {
id: `separator-between-${left.item.id}-and-${right.item.id}`,
},
render: getSeparator,
}),
items,
)
: identity,
map(({ render, item }) => (
<React.Fragment key={item.id}>{render()}</React.Fragment>
)),
)}
</>
);
};