Skip to content
Open
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
40 changes: 40 additions & 0 deletions bricks/basic/docs/eo-card-box.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
卡片项容器

## Examples

### Basic

```yaml preview
brick: eo-card-box
children:
- brick: eo-avatar
slot: avatar
properties:
icon:
lib: easyops
icon: account
color: var(--theme-blue-color)
bgColor: var(--theme-blue-background)
- brick: span
slot: title
properties:
textContent: Hello
- brick: span
slot: description
properties:
textContent: World
- brick: eo-tag-list
slot: footer
properties:
size: small
list:
- text: IT 资源管理
key: IT_resource_management
color: gray
- text: 资源套餐
key: resource_package
color: gray
- text: 存储设备
key: storage_device
color: gray
```
35 changes: 30 additions & 5 deletions bricks/basic/src/avatar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import React, {
import { createDecorators } from "@next-core/element";
import { ReactNextElement, wrapBrick } from "@next-core/react-element";
import "@next-core/theme";
import styleText from "./styles.shadow.css";
import type {
GeneralIcon,
GeneralIconProps,
} from "@next-bricks/icons/general-icon";
import classNames from "classnames";
import { isNil, omitBy } from "lodash";
import { WrappedSlResizeObserver } from "./sl-resize-observer.js";
import styleText from "./styles.shadow.css";

const { defineElement, property } = createDecorators();

Expand All @@ -35,6 +36,8 @@ export interface AvatarProps {
name?: string;
bordered?: boolean;
showName?: boolean;
color?: string;
bgColor?: string;
}

/**
Expand Down Expand Up @@ -78,6 +81,12 @@ class EoAvatar extends ReactNextElement implements AvatarProps {
})
accessor icon: GeneralIconProps | undefined;

/** 图标颜色 */
@property() accessor color: string | undefined;

/** 图标背景色 */
@property() accessor bgColor: string | undefined;

/**
* 用户名
*/
Expand Down Expand Up @@ -110,14 +119,25 @@ class EoAvatar extends ReactNextElement implements AvatarProps {
name={this.name}
bordered={this.bordered}
showName={this.showName}
color={this.color}
bgColor={this.bgColor}
/>
);
}
}

export function EoAvatarComponent(props: AvatarProps) {
const { shape, size, src, alt, icon, name, bordered } = props;

export function EoAvatarComponent({
shape,
size,
src,
alt,
icon,
name,
bordered,
showName: propShowName,
color,
bgColor,
}: AvatarProps) {
const avatarNodeRef = useRef<HTMLSpanElement>(null);
const textNodeRef = useRef<HTMLSpanElement>(null);

Expand Down Expand Up @@ -233,10 +253,15 @@ export function EoAvatarComponent(props: AvatarProps) {
ref={avatarNodeRef}
part={`avatar avatar-${type}`}
title={name}
style={
type === "icon"
? omitBy({ color, backgroundColor: bgColor }, isNil)
: undefined
}
>
{avatarNode}
</span>
{props.showName && <span className="name">{name}</span>}
{propShowName && <span className="name">{name}</span>}
</>
);
}
4 changes: 2 additions & 2 deletions bricks/basic/src/base-bricks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Merge bricks
import "./button/index.js";
import "./link/index.js";
import "./tooltip/index.js";
import "./button/index.js";
import "./data-providers/show-dialog/show-dialog.js";
import "./data-providers/show-notification/show-notification.js";
import "./tooltip/index.js";
1 change: 1 addition & 0 deletions bricks/basic/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ import "./data-providers/set-timeout.js";
import "./dropdown-select/index.js";
import "./loading-container/index.js";
import "./custom-processors/smartDisplayForEvaluableString.js";
import "./card-box/index.js";
85 changes: 85 additions & 0 deletions bricks/basic/src/card-box/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, test, expect, jest } from "@jest/globals";
import { act } from "react-dom/test-utils";
import { createHistory } from "@next-core/runtime";
import "../link/index";
import "./";
import type { CardBox } from "./index.js";

jest.mock("@next-core/theme", () => ({}));

createHistory();

describe("eo-card-box", () => {
test("basic usage", async () => {
const element = document.createElement("eo-card-box") as CardBox;
element.href = "/test";

const slots = ["avatar", "title", "description", "", "footer"];
const classNames = [
"avatar",
"title",
"description",
"content",
"footer",
"body",
"detail",
];
for (const slot of slots) {
const slotElement = document.createElement("div");
slotElement.slot = slot;
element.appendChild(slotElement);
}

act(() => {
document.body.appendChild(element);
});

expect(
element.shadowRoot?.querySelector("eo-link")?.getAttribute("href")
).toBe("/test");
expect(
element.shadowRoot
?.querySelector("eo-link")
?.classList.contains("clickable")
).toBe(true);

for (const className of classNames) {
expect(
element.shadowRoot
?.querySelector(`.${className}`)
?.classList.contains("hidden")
).toBe(false);
}

// Remove all children
await act(async () => {
element.replaceChildren();
});

for (const className of classNames) {
expect(
element.shadowRoot
?.querySelector(`.${className}`)
?.classList.contains("hidden")
).toBe(true);
}

// Unset href
element.href = undefined;
await act(async () => {
// wait
});
expect(
element.shadowRoot?.querySelector("eo-link")?.getAttribute("href")
).toBe(null);
expect(
element.shadowRoot
?.querySelector("eo-link")
?.classList.contains("clickable")
).toBe(false);

act(() => {
document.body.removeChild(element);
});
});
});
163 changes: 163 additions & 0 deletions bricks/basic/src/card-box/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import React, { useEffect, useRef, useState } from "react";
import { createDecorators } from "@next-core/element";
import { ReactNextElement, wrapBrick } from "@next-core/react-element";
import classNames from "classnames";
import "@next-core/theme";
import type {
ExtendedLocationDescriptor,
Link,
LinkProps,
Target,
} from "../link";
import styleText from "./styles.shadow.css";

const WrappedLink = wrapBrick<Link, LinkProps>("eo-link");

const { defineElement, property } = createDecorators();

export interface CardBoxProps
extends Pick<LinkProps, "url" | "href" | "target"> {
// Define props here.
}

/**
* 卡片项容器
*
* @slot avatar - 头像
* @slot title - 标题
* @slot description - 描述
* @slot - 内容区
* @slot footer - 底部
*/
export
@defineElement("eo-card-box", {
styleTexts: [styleText],
})
class CardBox extends ReactNextElement implements CardBoxProps {
/**
* 链接地址
*/
@property({
attribute: false,
})
accessor url: ExtendedLocationDescriptor | undefined;

/**
* 设置 `href` 时将使用原生 `<a>` 标签,通常用于外链的跳转
*/
@property() accessor href: string | undefined;

/**
* 链接跳转目标
*/
@property() accessor target: Target | undefined;

/** 是否铺满容器 */
@property({ type: Boolean, render: false })
accessor fillContainer: boolean | undefined;

render() {
return (
<CardBoxComponent url={this.url} href={this.href} target={this.target} />
);
}
}

export interface CardBoxComponentProps extends CardBoxProps {
// Define react event handlers here.
}

export function CardBoxComponent({ url, href, target }: CardBoxComponentProps) {
const slotsRef = useRef(new Map<string, HTMLSlotElement | null>());
const [slotsMap, setSlotsMap] = useState(() => new Map<string, boolean>());

useEffect(() => {
const slots = ["avatar", "title", "description", "", "footer"];
const disposables: (() => void)[] = [];

for (const slot of slots) {
const slotElement = slotsRef.current.get(slot);
if (slotElement) {
const onSlotChange = () => {
setSlotsMap((prev) => {
const prevHas = prev.get(slot) ?? false;
const currentHas = slotElement.assignedNodes().length > 0;
return prevHas === currentHas
? prev
: new Map(prev).set(slot, currentHas);
});
};
slotElement.addEventListener("slotchange", onSlotChange);
onSlotChange();
disposables.push(() => {
slotElement.removeEventListener("slotchange", onSlotChange);
});
}
}

return () => {
disposables.forEach((dispose) => dispose());
};
});

const refCallbackFactory =
(slot: string) => (element: HTMLSlotElement | null) => {
slotsRef.current.set(slot, element);
};

const getSlotContainerClassName = (slot: string, className?: string) => {
return classNames(className ?? slot, { hidden: !slotsMap.get(slot) });
};

const getSlotsContainerClassName = (slots: string[], className: string) => {
return classNames(className, {
hidden: slots.every((slot) => !slotsMap.get(slot)),
});
};

return (
<WrappedLink
className={classNames("box", { clickable: !!(url || href) })}
type="plain"
url={url}
href={href}
target={target}
>
{/* <div className="header">
<slot name="header" />
</div>
<div className="cover">
<slot name="cover" />
</div> */}
<div
className={getSlotsContainerClassName(
["avatar", "title", "description"],
"body"
)}
>
<div className={getSlotContainerClassName("avatar")}>
<slot name="avatar" ref={refCallbackFactory("avatar")} />
</div>
<div
className={getSlotsContainerClassName(
["title", "description"],
"detail"
)}
>
<div className={getSlotContainerClassName("title")}>
<slot name="title" ref={refCallbackFactory("title")} />
</div>
<div className={getSlotContainerClassName("description")}>
<slot name="description" ref={refCallbackFactory("description")} />
</div>
</div>
</div>
<div className={getSlotContainerClassName("", "content")}>
<slot ref={refCallbackFactory("")} />
</div>
<div className={getSlotContainerClassName("footer")}>
<slot name="footer" ref={refCallbackFactory("footer")} />
</div>
</WrappedLink>
);
}
Loading
Loading