@usefui/components 1.5.3 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +615 -51
- package/dist/index.d.ts +615 -51
- package/dist/index.js +3154 -660
- package/dist/index.mjs +3131 -661
- package/package.json +12 -12
- package/src/__tests__/Avatar.test.tsx +55 -55
- package/src/__tests__/MessageBubble.test.tsx +179 -0
- package/src/__tests__/Shimmer.test.tsx +122 -0
- package/src/__tests__/Tree.test.tsx +275 -0
- package/src/accordion/Accordion.stories.tsx +6 -4
- package/src/accordion/hooks/index.tsx +3 -1
- package/src/accordion/index.tsx +1 -2
- package/src/avatar/Avatar.stories.tsx +37 -7
- package/src/avatar/index.tsx +90 -19
- package/src/avatar/styles/index.ts +58 -12
- package/src/badge/Badge.stories.tsx +27 -5
- package/src/badge/index.tsx +21 -14
- package/src/badge/styles/index.ts +69 -40
- package/src/button/Button.stories.tsx +40 -27
- package/src/button/index.tsx +13 -9
- package/src/button/styles/index.ts +308 -47
- package/src/card/index.tsx +2 -4
- package/src/checkbox/Checkbox.stories.tsx +72 -33
- package/src/checkbox/hooks/index.tsx +5 -1
- package/src/checkbox/index.tsx +8 -6
- package/src/checkbox/styles/index.ts +239 -19
- package/src/collapsible/Collapsible.stories.tsx +6 -4
- package/src/collapsible/hooks/index.tsx +3 -1
- package/src/dialog/Dialog.stories.tsx +173 -31
- package/src/dialog/hooks/index.tsx +5 -1
- package/src/dialog/styles/index.ts +15 -8
- package/src/dropdown/Dropdown.stories.tsx +61 -23
- package/src/dropdown/hooks/index.tsx +3 -1
- package/src/dropdown/index.tsx +51 -40
- package/src/dropdown/styles/index.ts +30 -19
- package/src/field/Field.stories.tsx +183 -24
- package/src/field/hooks/index.tsx +5 -1
- package/src/field/index.tsx +930 -13
- package/src/field/styles/index.ts +246 -14
- package/src/field/types/index.ts +31 -0
- package/src/field/utils/index.ts +201 -0
- package/src/index.ts +8 -1
- package/src/message-bubble/MessageBubble.stories.tsx +138 -0
- package/src/message-bubble/hooks/index.tsx +41 -0
- package/src/message-bubble/index.tsx +171 -0
- package/src/message-bubble/styles/index.ts +58 -0
- package/src/otp-field/OTPField.stories.tsx +22 -24
- package/src/otp-field/hooks/index.tsx +3 -1
- package/src/otp-field/index.tsx +14 -3
- package/src/otp-field/styles/index.ts +114 -16
- package/src/otp-field/types/index.ts +9 -1
- package/src/overlay/styles/index.ts +1 -0
- package/src/ruler/Ruler.stories.tsx +43 -0
- package/src/ruler/constants/index.ts +3 -0
- package/src/ruler/hooks/index.tsx +53 -0
- package/src/ruler/index.tsx +239 -0
- package/src/ruler/styles/index.tsx +154 -0
- package/src/ruler/types/index.ts +17 -0
- package/src/select/Select.stories.tsx +91 -0
- package/src/select/hooks/index.tsx +71 -0
- package/src/select/index.tsx +331 -0
- package/src/select/styles/index.tsx +156 -0
- package/src/sheet/hooks/index.tsx +5 -1
- package/src/shimmer/Shimmer.stories.tsx +97 -0
- package/src/shimmer/index.tsx +64 -0
- package/src/shimmer/styles/index.ts +33 -0
- package/src/skeleton/index.tsx +7 -6
- package/src/spinner/Spinner.stories.tsx +29 -4
- package/src/spinner/index.tsx +16 -6
- package/src/spinner/styles/index.ts +41 -22
- package/src/switch/Switch.stories.tsx +46 -17
- package/src/switch/hooks/index.tsx +5 -1
- package/src/switch/index.tsx +5 -8
- package/src/switch/styles/index.ts +45 -45
- package/src/tabs/Tabs.stories.tsx +43 -15
- package/src/tabs/hooks/index.tsx +5 -1
- package/src/text-area/Textarea.stories.tsx +45 -8
- package/src/text-area/index.tsx +9 -6
- package/src/text-area/styles/index.ts +1 -1
- package/src/toggle/Toggle.stories.tsx +6 -4
- package/src/toolbar/hooks/index.tsx +5 -1
- package/src/tree/Tree.stories.tsx +141 -0
- package/src/tree/hooks/tree-node-provider.tsx +50 -0
- package/src/tree/hooks/tree-provider.tsx +75 -0
- package/src/tree/index.tsx +231 -0
- package/src/tree/styles/index.ts +23 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +1 -3
- package/src/privacy-field/PrivacyField.stories.tsx +0 -29
- package/src/privacy-field/index.tsx +0 -56
- package/src/privacy-field/styles/index.ts +0 -17
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { test, vi, afterEach, describe, expect } from "vitest";
|
|
4
|
+
import {
|
|
5
|
+
screen,
|
|
6
|
+
render,
|
|
7
|
+
cleanup,
|
|
8
|
+
waitFor,
|
|
9
|
+
fireEvent,
|
|
10
|
+
} from "@testing-library/react";
|
|
11
|
+
import { axe, toHaveNoViolations } from "jest-axe";
|
|
12
|
+
|
|
13
|
+
import { Tree } from "../../src/tree";
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
vi.resetModules();
|
|
18
|
+
cleanup();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect.extend(toHaveNoViolations);
|
|
22
|
+
|
|
23
|
+
describe("Tree", () => {
|
|
24
|
+
test("Renders without accessibility violation", async () => {
|
|
25
|
+
const { container } = render(
|
|
26
|
+
<Tree.Root>
|
|
27
|
+
<Tree>
|
|
28
|
+
<Tree.Node nodeId="node-1">
|
|
29
|
+
<Tree.Trigger nodeId="node-1">Node 1</Tree.Trigger>
|
|
30
|
+
<Tree.Content nodeId="node-1" defaultOpen>
|
|
31
|
+
<Tree.Node level={1} nodeId="node-1-child">
|
|
32
|
+
<Tree.Trigger nodeId="node-1-child">Node 1 Child</Tree.Trigger>
|
|
33
|
+
</Tree.Node>
|
|
34
|
+
</Tree.Content>
|
|
35
|
+
</Tree.Node>
|
|
36
|
+
</Tree>
|
|
37
|
+
</Tree.Root>,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const ComponentContainer = await axe(container);
|
|
41
|
+
expect(ComponentContainer).toHaveNoViolations();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// test("Renders with accessibility definition", async () => {
|
|
45
|
+
// render(
|
|
46
|
+
// <Tree.Root>
|
|
47
|
+
// <Tree>
|
|
48
|
+
// <Tree.Node nodeId="node-1">
|
|
49
|
+
// <Tree.Trigger nodeId="node-1">Node 1</Tree.Trigger>
|
|
50
|
+
// <Tree.Content nodeId="node-1">
|
|
51
|
+
// <Tree.Node level={1} nodeId="node-1-child">
|
|
52
|
+
// <Tree.Trigger nodeId="node-1-child">Node 1 Child</Tree.Trigger>
|
|
53
|
+
// </Tree.Node>
|
|
54
|
+
// </Tree.Content>
|
|
55
|
+
// </Tree.Node>
|
|
56
|
+
// </Tree>
|
|
57
|
+
// </Tree.Root>,
|
|
58
|
+
// );
|
|
59
|
+
|
|
60
|
+
// const treeView = screen.getByRole("tree");
|
|
61
|
+
// expect(treeView).toBeDefined();
|
|
62
|
+
|
|
63
|
+
// const treeItems = screen.getAllByRole("treeitem");
|
|
64
|
+
// expect(treeItems.length).toBe(1);
|
|
65
|
+
|
|
66
|
+
// expect(() => screen.getByRole("group")).toThrow();
|
|
67
|
+
|
|
68
|
+
// const trigger = screen.getByLabelText("node-1-action");
|
|
69
|
+
// expect(trigger).toBeDefined();
|
|
70
|
+
// expect(trigger.getAttribute("aria-expanded")).toBe("false");
|
|
71
|
+
// expect(trigger.getAttribute("aria-selected")).toBe("false");
|
|
72
|
+
// expect(trigger.getAttribute("data-state")).toBe("collapsed");
|
|
73
|
+
|
|
74
|
+
// fireEvent.click(trigger);
|
|
75
|
+
// await waitFor(() => {
|
|
76
|
+
// const group = screen.getByRole("group");
|
|
77
|
+
// expect(group).toBeDefined();
|
|
78
|
+
// expect(group.getAttribute("aria-labelledby")).toBeDefined();
|
|
79
|
+
// expect(group.getAttribute("id")).toBeDefined();
|
|
80
|
+
// expect(group.getAttribute("data-nodeid")).toBe("node-1");
|
|
81
|
+
|
|
82
|
+
// expect(trigger.getAttribute("aria-expanded")).toBe("true");
|
|
83
|
+
// expect(trigger.getAttribute("aria-selected")).toBe("true");
|
|
84
|
+
// expect(trigger.getAttribute("data-state")).toBe("expanded");
|
|
85
|
+
|
|
86
|
+
// const childItems = screen.getAllByRole("treeitem");
|
|
87
|
+
// expect(childItems.length).toBe(2);
|
|
88
|
+
// });
|
|
89
|
+
// });
|
|
90
|
+
|
|
91
|
+
test("Renders with correct aria-level on nested nodes", async () => {
|
|
92
|
+
render(
|
|
93
|
+
<Tree.Root>
|
|
94
|
+
<Tree>
|
|
95
|
+
<Tree.Node nodeId="root">
|
|
96
|
+
<Tree.Trigger nodeId="root">Root</Tree.Trigger>
|
|
97
|
+
<Tree.Content nodeId="root" defaultOpen>
|
|
98
|
+
<Tree.Node level={1} nodeId="child">
|
|
99
|
+
<Tree.Trigger nodeId="child">Child</Tree.Trigger>
|
|
100
|
+
<Tree.Content nodeId="child" defaultOpen>
|
|
101
|
+
<Tree.Node level={2} nodeId="grandchild">
|
|
102
|
+
<Tree.Trigger nodeId="grandchild">Grandchild</Tree.Trigger>
|
|
103
|
+
</Tree.Node>
|
|
104
|
+
</Tree.Content>
|
|
105
|
+
</Tree.Node>
|
|
106
|
+
</Tree.Content>
|
|
107
|
+
</Tree.Node>
|
|
108
|
+
</Tree>
|
|
109
|
+
</Tree.Root>,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const treeItems = screen.getAllByRole("treeitem");
|
|
113
|
+
expect((treeItems[0] as HTMLElement).getAttribute("aria-level")).toBe("1");
|
|
114
|
+
expect((treeItems[1] as HTMLElement).getAttribute("aria-level")).toBe("2");
|
|
115
|
+
expect((treeItems[2] as HTMLElement).getAttribute("aria-level")).toBe("3");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// test("Renders with defaultOpen and expands content on mount", async () => {
|
|
119
|
+
// render(
|
|
120
|
+
// <Tree.Root>
|
|
121
|
+
// <Tree>
|
|
122
|
+
// <Tree.Node nodeId="node-1">
|
|
123
|
+
// <Tree.Trigger nodeId="node-1">Node 1</Tree.Trigger>
|
|
124
|
+
// <Tree.Content nodeId="node-1" defaultOpen>
|
|
125
|
+
// <Tree.Node level={1} nodeId="node-1-child">
|
|
126
|
+
// <Tree.Trigger nodeId="node-1-child">Node 1 Child</Tree.Trigger>
|
|
127
|
+
// </Tree.Node>
|
|
128
|
+
// </Tree.Content>
|
|
129
|
+
// </Tree.Node>
|
|
130
|
+
// </Tree>
|
|
131
|
+
// </Tree.Root>,
|
|
132
|
+
// );
|
|
133
|
+
|
|
134
|
+
// await waitFor(() => {
|
|
135
|
+
// expect(screen.getByRole("group")).toBeDefined();
|
|
136
|
+
// expect(screen.getByText("Node 1 Child")).toBeDefined();
|
|
137
|
+
|
|
138
|
+
// const trigger = screen.getByLabelText("node-1-action");
|
|
139
|
+
// expect(trigger.getAttribute("aria-expanded")).toBe("true");
|
|
140
|
+
// expect(trigger.getAttribute("data-state")).toBe("expanded");
|
|
141
|
+
// });
|
|
142
|
+
// });
|
|
143
|
+
|
|
144
|
+
test("Fires the defined callback and toggles content when trigger is clicked", async () => {
|
|
145
|
+
const onClickCallback = vi.fn();
|
|
146
|
+
|
|
147
|
+
render(
|
|
148
|
+
<Tree.Root>
|
|
149
|
+
<Tree>
|
|
150
|
+
<Tree.Node nodeId="node-1">
|
|
151
|
+
<Tree.Trigger
|
|
152
|
+
nodeId="node-1"
|
|
153
|
+
name="trigger-1"
|
|
154
|
+
onClick={onClickCallback}
|
|
155
|
+
>
|
|
156
|
+
Node 1
|
|
157
|
+
</Tree.Trigger>
|
|
158
|
+
<Tree.Content nodeId="node-1" defaultOpen>
|
|
159
|
+
<Tree.Node level={1} nodeId="node-1-child">
|
|
160
|
+
<Tree.Trigger nodeId="node-1-child">Node 1 Child</Tree.Trigger>
|
|
161
|
+
</Tree.Node>
|
|
162
|
+
</Tree.Content>
|
|
163
|
+
</Tree.Node>
|
|
164
|
+
<Tree.Node nodeId="node-2">
|
|
165
|
+
<Tree.Trigger nodeId="node-2" name="trigger-2">
|
|
166
|
+
Node 2
|
|
167
|
+
</Tree.Trigger>
|
|
168
|
+
<Tree.Content nodeId="node-2">
|
|
169
|
+
<Tree.Node level={1} nodeId="node-2-child">
|
|
170
|
+
<Tree.Trigger nodeId="node-2-child">Node 2 Child</Tree.Trigger>
|
|
171
|
+
</Tree.Node>
|
|
172
|
+
</Tree.Content>
|
|
173
|
+
</Tree.Node>
|
|
174
|
+
</Tree>
|
|
175
|
+
</Tree.Root>,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(screen.getByText("Node 1 Child")).toBeDefined();
|
|
179
|
+
expect(() => screen.getByText("Node 2 Child")).toThrow();
|
|
180
|
+
|
|
181
|
+
fireEvent.click(screen.getByLabelText("trigger-2-action"));
|
|
182
|
+
await waitFor(() => {
|
|
183
|
+
expect(screen.getByText("Node 2 Child")).toBeDefined();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
fireEvent.click(screen.getByLabelText("trigger-1-action"));
|
|
187
|
+
await waitFor(() => {
|
|
188
|
+
expect(() => screen.getByText("Node 1 Child")).toThrow();
|
|
189
|
+
expect(onClickCallback).toHaveBeenCalledTimes(1);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
fireEvent.click(screen.getByLabelText("trigger-1-action"));
|
|
193
|
+
await waitFor(() => {
|
|
194
|
+
expect(screen.getByText("Node 1 Child")).toBeDefined();
|
|
195
|
+
expect(onClickCallback).toHaveBeenCalledTimes(2);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
fireEvent.click(screen.getByLabelText("trigger-1-action"));
|
|
199
|
+
await waitFor(() => {
|
|
200
|
+
expect(() => screen.getByText("Node 1 Child")).toThrow();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("Fires onSelectionChange callback with selected node ids", async () => {
|
|
205
|
+
const onSelectionChange = vi.fn();
|
|
206
|
+
|
|
207
|
+
render(
|
|
208
|
+
<Tree.Root onSelectionChange={onSelectionChange}>
|
|
209
|
+
<Tree>
|
|
210
|
+
<Tree.Node nodeId="node-1">
|
|
211
|
+
<Tree.Trigger nodeId="node-1" name="trigger-1">
|
|
212
|
+
Node 1
|
|
213
|
+
</Tree.Trigger>
|
|
214
|
+
</Tree.Node>
|
|
215
|
+
<Tree.Node nodeId="node-2">
|
|
216
|
+
<Tree.Trigger nodeId="node-2" name="trigger-2">
|
|
217
|
+
Node 2
|
|
218
|
+
</Tree.Trigger>
|
|
219
|
+
</Tree.Node>
|
|
220
|
+
</Tree>
|
|
221
|
+
</Tree.Root>,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
fireEvent.click(screen.getByLabelText("trigger-1-action"));
|
|
225
|
+
await waitFor(() => {
|
|
226
|
+
expect(onSelectionChange).toHaveBeenCalledTimes(1);
|
|
227
|
+
expect(onSelectionChange).toHaveBeenCalledWith(["node-1"]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
fireEvent.click(screen.getByLabelText("trigger-2-action"));
|
|
231
|
+
await waitFor(() => {
|
|
232
|
+
expect(onSelectionChange).toHaveBeenCalledTimes(2);
|
|
233
|
+
expect(onSelectionChange).toHaveBeenCalledWith(["node-1", "node-2"]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
fireEvent.click(screen.getByLabelText("trigger-1-action"));
|
|
237
|
+
await waitFor(() => {
|
|
238
|
+
expect(onSelectionChange).toHaveBeenCalledTimes(3);
|
|
239
|
+
expect(onSelectionChange).toHaveBeenCalledWith(["node-2"]);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("Renders with defaultExpandedIds and expands matching nodes on mount", async () => {
|
|
244
|
+
render(
|
|
245
|
+
<Tree.Root defaultExpandedIds={["node-1", "node-2"]}>
|
|
246
|
+
<Tree>
|
|
247
|
+
<Tree.Node nodeId="node-1">
|
|
248
|
+
<Tree.Trigger nodeId="node-1">Node 1</Tree.Trigger>
|
|
249
|
+
<Tree.Content nodeId="node-1">
|
|
250
|
+
<Tree.Node level={1} nodeId="node-1-child">
|
|
251
|
+
<Tree.Trigger nodeId="node-1-child">Node 1 Child</Tree.Trigger>
|
|
252
|
+
</Tree.Node>
|
|
253
|
+
</Tree.Content>
|
|
254
|
+
</Tree.Node>
|
|
255
|
+
<Tree.Node nodeId="node-2">
|
|
256
|
+
<Tree.Trigger nodeId="node-2">Node 2</Tree.Trigger>
|
|
257
|
+
<Tree.Content nodeId="node-2">
|
|
258
|
+
<Tree.Node level={1} nodeId="node-2-child">
|
|
259
|
+
<Tree.Trigger nodeId="node-2-child">Node 2 Child</Tree.Trigger>
|
|
260
|
+
</Tree.Node>
|
|
261
|
+
</Tree.Content>
|
|
262
|
+
</Tree.Node>
|
|
263
|
+
</Tree>
|
|
264
|
+
</Tree.Root>,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
await waitFor(() => {
|
|
268
|
+
expect(screen.getByText("Node 1 Child")).toBeDefined();
|
|
269
|
+
expect(screen.getByText("Node 2 Child")).toBeDefined();
|
|
270
|
+
|
|
271
|
+
const groups = screen.getAllByRole("group");
|
|
272
|
+
expect(groups.length).toBe(2);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
|
|
4
|
-
import { Accordion } from "..";
|
|
4
|
+
import { Accordion, Page } from "..";
|
|
5
5
|
import { ComponentSizeEnum, ComponentVariantEnum } from "../../../../types";
|
|
6
6
|
|
|
7
7
|
const meta = {
|
|
@@ -10,9 +10,11 @@ const meta = {
|
|
|
10
10
|
tags: ["autodocs"],
|
|
11
11
|
decorators: [
|
|
12
12
|
(Story) => (
|
|
13
|
-
<
|
|
14
|
-
<
|
|
15
|
-
|
|
13
|
+
<Page>
|
|
14
|
+
<Page.Content className="p-medium-30">
|
|
15
|
+
<Story />
|
|
16
|
+
</Page.Content>
|
|
17
|
+
</Page>
|
|
16
18
|
),
|
|
17
19
|
],
|
|
18
20
|
} satisfies Meta<typeof Accordion>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import React, { useState, createContext, useContext } from "react";
|
|
2
4
|
import { IReactChildren, IComponentAPI } from "../../../../../types";
|
|
3
5
|
|
|
@@ -11,7 +13,7 @@ export const useAccordion = () => useContext(AccordionContext);
|
|
|
11
13
|
|
|
12
14
|
export const AccordionProvider = ({
|
|
13
15
|
children,
|
|
14
|
-
}: IReactChildren): JSX.Element => {
|
|
16
|
+
}: IReactChildren): React.JSX.Element => {
|
|
15
17
|
const context = useAccordionProvider();
|
|
16
18
|
|
|
17
19
|
return (
|
package/src/accordion/index.tsx
CHANGED
|
@@ -16,8 +16,7 @@ export interface IAccordionComposition {
|
|
|
16
16
|
Content: typeof AccordionContent;
|
|
17
17
|
}
|
|
18
18
|
export interface IAccordionProperties
|
|
19
|
-
extends IComponentSpacing,
|
|
20
|
-
React.ComponentProps<"div"> {}
|
|
19
|
+
extends IComponentSpacing, React.ComponentProps<"div"> {}
|
|
21
20
|
export interface IAccordionTriggerProperties extends IButtonProperties {
|
|
22
21
|
value: string;
|
|
23
22
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
-
import { Avatar } from "..";
|
|
3
|
+
import { Avatar, Page, Tooltip } from "..";
|
|
4
4
|
|
|
5
5
|
const meta = {
|
|
6
6
|
title: "Components/Avatar",
|
|
@@ -8,9 +8,11 @@ const meta = {
|
|
|
8
8
|
tags: ["autodocs"],
|
|
9
9
|
decorators: [
|
|
10
10
|
(Story) => (
|
|
11
|
-
<
|
|
12
|
-
<
|
|
13
|
-
|
|
11
|
+
<Page>
|
|
12
|
+
<Page.Content className="p-medium-30">
|
|
13
|
+
<Story />
|
|
14
|
+
</Page.Content>
|
|
15
|
+
</Page>
|
|
14
16
|
),
|
|
15
17
|
],
|
|
16
18
|
} satisfies Meta<typeof Avatar>;
|
|
@@ -31,6 +33,25 @@ export const Status: Story = {
|
|
|
31
33
|
</div>
|
|
32
34
|
),
|
|
33
35
|
};
|
|
36
|
+
export const Badges: Story = {
|
|
37
|
+
render: ({ ...args }) => (
|
|
38
|
+
<div className="flex g-medium-30">
|
|
39
|
+
{["small", "medium", "large"].map((variant) => (
|
|
40
|
+
<Avatar
|
|
41
|
+
sizing={variant as "small"}
|
|
42
|
+
alt="foundation-logo"
|
|
43
|
+
src="https://www.untitledui.com/images/avatars/olivia-rhye?fm=webp&q=80"
|
|
44
|
+
>
|
|
45
|
+
<Avatar.Badge
|
|
46
|
+
alt="foundation-logo"
|
|
47
|
+
src="https://www.untitledui.com/logos/images/Layers.jpg"
|
|
48
|
+
/>
|
|
49
|
+
</Avatar>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
),
|
|
53
|
+
};
|
|
54
|
+
|
|
34
55
|
export const Sizes: Story = {
|
|
35
56
|
render: ({ ...args }) => (
|
|
36
57
|
<div className="flex g-medium-30">
|
|
@@ -40,22 +61,31 @@ export const Sizes: Story = {
|
|
|
40
61
|
</div>
|
|
41
62
|
),
|
|
42
63
|
};
|
|
64
|
+
export const Shapes: Story = {
|
|
65
|
+
render: ({ ...args }) => (
|
|
66
|
+
<div className="flex g-medium-30">
|
|
67
|
+
<Avatar shape="square" status="online" {...args} />
|
|
68
|
+
<Avatar shape="smooth" status="online" {...args} />
|
|
69
|
+
<Avatar shape="round" status="online" {...args} />
|
|
70
|
+
</div>
|
|
71
|
+
),
|
|
72
|
+
};
|
|
43
73
|
export const Variants: Story = {
|
|
44
74
|
render: ({ ...args }) => (
|
|
45
75
|
<div className="flex g-medium-30">
|
|
46
76
|
<Avatar />
|
|
47
77
|
<Avatar
|
|
48
78
|
alt="foundation-logo"
|
|
49
|
-
src="https://
|
|
79
|
+
src="https://www.untitledui.com/images/avatars/olivia-rhye?fm=webp&q=80"
|
|
50
80
|
/>
|
|
51
81
|
<Avatar>
|
|
52
|
-
<b>AZ</b>
|
|
82
|
+
<b className="fs-medium-10">AZ</b>
|
|
53
83
|
</Avatar>
|
|
54
84
|
<Avatar
|
|
55
85
|
style={{ backgroundColor: "var(--color-purple)" }}
|
|
56
86
|
status="online"
|
|
57
87
|
>
|
|
58
|
-
<small>Acme</
|
|
88
|
+
<span className="fs-small-30">Acme</span>
|
|
59
89
|
</Avatar>
|
|
60
90
|
</div>
|
|
61
91
|
),
|
package/src/avatar/index.tsx
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React from "react";
|
|
4
|
-
import { AvatarWrapper, StatusWrapper } from "./styles";
|
|
4
|
+
import { AvatarWrapper, BadgeWrapper, StatusWrapper } from "./styles";
|
|
5
5
|
import {
|
|
6
6
|
IComponentStyling,
|
|
7
7
|
ComponentSizeEnum,
|
|
8
8
|
IComponentSize,
|
|
9
|
+
ComponentShapeEnum,
|
|
10
|
+
IComponentShape,
|
|
9
11
|
} from "../../../../types";
|
|
10
12
|
|
|
11
13
|
export enum AvataStatusEnum {
|
|
@@ -14,13 +16,25 @@ export enum AvataStatusEnum {
|
|
|
14
16
|
Busy = "busy",
|
|
15
17
|
Offline = "offline",
|
|
16
18
|
}
|
|
19
|
+
export type AvatarStatusType = "online" | "away" | "busy" | "offline";
|
|
17
20
|
export interface IAvatarProperties
|
|
18
|
-
extends
|
|
21
|
+
extends
|
|
22
|
+
IComponentStyling,
|
|
19
23
|
IComponentSize,
|
|
24
|
+
IComponentShape,
|
|
20
25
|
React.HTMLAttributes<HTMLDivElement> {
|
|
21
26
|
src?: string;
|
|
22
27
|
alt?: string;
|
|
23
|
-
status?:
|
|
28
|
+
status?: AvatarStatusType;
|
|
29
|
+
}
|
|
30
|
+
export interface IAvatarBadgeProperties extends React.HTMLAttributes<HTMLDivElement> {
|
|
31
|
+
src?: string;
|
|
32
|
+
alt?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface IAvatarComposition {
|
|
36
|
+
Status: typeof AvatarStatus;
|
|
37
|
+
Badge: typeof AvatarBadge;
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
/**
|
|
@@ -41,11 +55,12 @@ export interface IAvatarProperties
|
|
|
41
55
|
* @param {ReactNode} props.children - The content to be rendered inside the avatar.
|
|
42
56
|
* @returns {ReactElement} The Avatar component.
|
|
43
57
|
*/
|
|
44
|
-
|
|
58
|
+
const Avatar = (props: IAvatarProperties) => {
|
|
45
59
|
const {
|
|
46
60
|
raw,
|
|
47
61
|
sizing = ComponentSizeEnum.Medium,
|
|
48
62
|
status,
|
|
63
|
+
shape = ComponentShapeEnum.Round,
|
|
49
64
|
src,
|
|
50
65
|
alt,
|
|
51
66
|
children,
|
|
@@ -58,10 +73,11 @@ export const Avatar = (props: IAvatarProperties) => {
|
|
|
58
73
|
data-raw={Boolean(raw)}
|
|
59
74
|
data-size={sizing}
|
|
60
75
|
data-status={status}
|
|
76
|
+
data-shape={shape}
|
|
61
77
|
aria-label={props["aria-label"] ?? `${sizeLabel}-user-avatar`}
|
|
62
78
|
{...restProps}
|
|
63
79
|
>
|
|
64
|
-
{
|
|
80
|
+
{src && (
|
|
65
81
|
<img
|
|
66
82
|
aria-label={`${sizeLabel}-user-avatar-image`}
|
|
67
83
|
alt={alt ?? `${sizeLabel}-user-avatar-image`}
|
|
@@ -70,21 +86,76 @@ export const Avatar = (props: IAvatarProperties) => {
|
|
|
70
86
|
)}
|
|
71
87
|
|
|
72
88
|
{children}
|
|
73
|
-
{status &&
|
|
74
|
-
<StatusWrapper
|
|
75
|
-
role="img"
|
|
76
|
-
aria-label={`${sizing}-user-avatar-status`}
|
|
77
|
-
aria-labelledby="title desc"
|
|
78
|
-
data-status={status}
|
|
79
|
-
height="16"
|
|
80
|
-
width="16"
|
|
81
|
-
>
|
|
82
|
-
<title>{"Activity status"}</title>
|
|
83
|
-
<desc>{status}</desc>
|
|
84
|
-
<circle role="presentation" cx="8" cy="8" r="6" />
|
|
85
|
-
</StatusWrapper>
|
|
86
|
-
)}
|
|
89
|
+
{status && <Avatar.Status status={status} />}
|
|
87
90
|
</AvatarWrapper>
|
|
88
91
|
);
|
|
89
92
|
};
|
|
90
93
|
Avatar.displayName = "Avatar";
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Avatar Statuses are used to represents a user activity status on an interface.
|
|
97
|
+
*
|
|
98
|
+
* **Best practices:**
|
|
99
|
+
*
|
|
100
|
+
* - Use the appropriate status to match the context and the importance of the information.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} props.status - The status of the user represented by the avatar.
|
|
103
|
+
* @returns {ReactElement} The Avatar Status component.
|
|
104
|
+
*/
|
|
105
|
+
const AvatarStatus = (props: {
|
|
106
|
+
status?: AvatarStatusType;
|
|
107
|
+
}): React.ReactElement => {
|
|
108
|
+
const { status } = props;
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<StatusWrapper
|
|
112
|
+
role="img"
|
|
113
|
+
aria-label={`${status}-user-avatar-status`}
|
|
114
|
+
aria-labelledby="title desc"
|
|
115
|
+
data-status={status}
|
|
116
|
+
height="12"
|
|
117
|
+
width="12"
|
|
118
|
+
{...props}
|
|
119
|
+
>
|
|
120
|
+
<title>{"Activity status"}</title>
|
|
121
|
+
<desc>{status}</desc>
|
|
122
|
+
<circle role="presentation" cx="6" cy="6" r="4" />
|
|
123
|
+
</StatusWrapper>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
AvatarStatus.displayName = "Avatar.Status";
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Avatar Badges are used to represents an entity or an information related to the primary subject of the avatar.
|
|
130
|
+
*
|
|
131
|
+
* **Best practices:**
|
|
132
|
+
*
|
|
133
|
+
* - Use the appropriate imgage to display the context and the importance of the information.
|
|
134
|
+
* - Always provide an `alt` attribute for accessibility when using an image.
|
|
135
|
+
*
|
|
136
|
+
* @param {IAvatarBadgeProperties} props - The props for the Avatar Badge component.
|
|
137
|
+
* @param {string} props.src - The source URL of the image to be displayed in the avatar badge.
|
|
138
|
+
* @param {string} props.alt - The alternative text for the image in the avatar badge.
|
|
139
|
+
* @returns {ReactElement} The Avatar Badge component.
|
|
140
|
+
*/
|
|
141
|
+
const AvatarBadge = (props: IAvatarBadgeProperties): React.ReactElement => {
|
|
142
|
+
const { src, alt, children } = props;
|
|
143
|
+
return (
|
|
144
|
+
<BadgeWrapper role="img" aria-label="user-avatar-badge-wrapper" {...props}>
|
|
145
|
+
{src && !children && (
|
|
146
|
+
<img
|
|
147
|
+
aria-label="user-avatar-badge"
|
|
148
|
+
alt={alt ?? "user-avatar-badge"}
|
|
149
|
+
src={src}
|
|
150
|
+
/>
|
|
151
|
+
)}
|
|
152
|
+
{!src && children && children}
|
|
153
|
+
</BadgeWrapper>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
AvatarBadge.displayName = "Avatar.Badge";
|
|
157
|
+
|
|
158
|
+
Avatar.Status = AvatarStatus;
|
|
159
|
+
Avatar.Badge = AvatarBadge;
|
|
160
|
+
|
|
161
|
+
export { Avatar, AvatarStatus };
|
|
@@ -2,19 +2,19 @@ import styled, { css } from "styled-components";
|
|
|
2
2
|
|
|
3
3
|
const AvatarSizesStyles = css`
|
|
4
4
|
&[data-size="small"] {
|
|
5
|
+
width: var(--measurement-medium-70);
|
|
6
|
+
height: var(--measurement-medium-70);
|
|
7
|
+
min-width: var(--measurement-medium-70);
|
|
8
|
+
min-height: var(--measurement-medium-70);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
&[data-size="medium"] {
|
|
5
12
|
width: var(--measurement-large-10);
|
|
6
13
|
height: var(--measurement-large-10);
|
|
7
14
|
min-width: var(--measurement-large-10);
|
|
8
15
|
min-height: var(--measurement-large-10);
|
|
9
16
|
}
|
|
10
17
|
|
|
11
|
-
&[data-size="medium"] {
|
|
12
|
-
width: var(--measurement-medium-90);
|
|
13
|
-
height: var(--measurement-medium-90);
|
|
14
|
-
min-width: var(--measurement-medium-90);
|
|
15
|
-
min-height: var(--measurement-medium-90);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
18
|
&[data-size="large"] {
|
|
19
19
|
width: var(--measurement-large-20);
|
|
20
20
|
height: var(--measurement-large-20);
|
|
@@ -22,6 +22,26 @@ const AvatarSizesStyles = css`
|
|
|
22
22
|
min-height: var(--measurement-large-20);
|
|
23
23
|
}
|
|
24
24
|
`;
|
|
25
|
+
const AvatarShapesStyles = css`
|
|
26
|
+
&[data-shape="square"] {
|
|
27
|
+
border-radius: 0;
|
|
28
|
+
img {
|
|
29
|
+
border-radius: 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
&[data-shape="smooth"] {
|
|
33
|
+
border-radius: var(--measurement-medium-30);
|
|
34
|
+
img {
|
|
35
|
+
border-radius: var(--measurement-medium-30);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
&[data-shape="round"] {
|
|
39
|
+
border-radius: 100%;
|
|
40
|
+
img {
|
|
41
|
+
border-radius: 100%;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
25
45
|
const AvatarStatusesStyles = css`
|
|
26
46
|
&[data-status="online"] {
|
|
27
47
|
fill: var(--shade-green-10);
|
|
@@ -40,7 +60,7 @@ const AvatarStatusesStyles = css`
|
|
|
40
60
|
|
|
41
61
|
&[data-status="offline"] {
|
|
42
62
|
fill: var(--body-color);
|
|
43
|
-
stroke: var(--
|
|
63
|
+
stroke: var(--contrast-color);
|
|
44
64
|
}
|
|
45
65
|
`;
|
|
46
66
|
|
|
@@ -51,17 +71,18 @@ export const AvatarWrapper = styled.div`
|
|
|
51
71
|
align-items: center;
|
|
52
72
|
justify-content: center;
|
|
53
73
|
|
|
54
|
-
background-color: var(--
|
|
55
|
-
border
|
|
74
|
+
background-color: var(--font-color-alpha-10);
|
|
75
|
+
border: var(--measurement-small-10) solid var(--font-color-alpha-10);
|
|
56
76
|
|
|
57
77
|
img {
|
|
58
78
|
width: inherit;
|
|
59
79
|
height: inherit;
|
|
60
80
|
min-width: inherit;
|
|
61
81
|
min-height: inherit;
|
|
62
|
-
border
|
|
82
|
+
border: var(--measurement-small-10) solid var(--font-color-alpha-10);
|
|
63
83
|
}
|
|
64
84
|
|
|
85
|
+
${AvatarShapesStyles}
|
|
65
86
|
${AvatarSizesStyles}
|
|
66
87
|
}
|
|
67
88
|
`;
|
|
@@ -71,9 +92,34 @@ export const StatusWrapper = styled.svg`
|
|
|
71
92
|
);
|
|
72
93
|
|
|
73
94
|
position: absolute;
|
|
74
|
-
stroke-width: var(--measurement-small-
|
|
95
|
+
stroke-width: var(--measurement-small-10);
|
|
75
96
|
bottom: var(--status-position);
|
|
76
97
|
right: var(--status-position);
|
|
77
98
|
|
|
78
99
|
${AvatarStatusesStyles}
|
|
79
100
|
`;
|
|
101
|
+
export const BadgeWrapper = styled.div`
|
|
102
|
+
--status-position: calc(
|
|
103
|
+
var(--measurement-medium-10) - (var(--measurement-medium-10) * 2)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
position: absolute;
|
|
107
|
+
|
|
108
|
+
bottom: var(--status-position);
|
|
109
|
+
right: var(--status-position);
|
|
110
|
+
|
|
111
|
+
width: var(--measurement-medium-60);
|
|
112
|
+
height: var(--measurement-medium-60);
|
|
113
|
+
|
|
114
|
+
background-color: var(--font-color-alpha-10);
|
|
115
|
+
border-radius: 100%;
|
|
116
|
+
|
|
117
|
+
img {
|
|
118
|
+
width: inherit;
|
|
119
|
+
height: inherit;
|
|
120
|
+
min-width: inherit;
|
|
121
|
+
min-height: inherit;
|
|
122
|
+
border-radius: 100%;
|
|
123
|
+
border: var(--measurement-small-10) solid var(--font-color-alpha-10);
|
|
124
|
+
}
|
|
125
|
+
`;
|