Skip to content

Commit 631a407

Browse files
committed
feat: Create composer Banner shared component.
1 parent dd89cee commit 631a407

11 files changed

Lines changed: 743 additions & 62 deletions

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<testExecutions version="1">
2+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/utils/humanize.test.ts">
3+
<testCase name="humanizeTime returns &apos;a few seconds ago&apos; for &lt;15s ago" duration="33"/>
4+
<testCase name="humanizeTime returns &apos;about a minute ago&apos; for &lt;75s ago" duration="1"/>
5+
<testCase name="humanizeTime returns &apos;20 minutes ago&apos; for &lt;45min ago" duration="1"/>
6+
<testCase name="humanizeTime returns &apos;about an hour ago&apos; for &lt;75min ago" duration="1"/>
7+
<testCase name="humanizeTime returns &apos;5 hours ago&apos; for &lt;23h ago" duration="1"/>
8+
<testCase name="humanizeTime returns &apos;about a day ago&apos; for &lt;26h ago" duration="0"/>
9+
<testCase name="humanizeTime returns &apos;3 days ago&apos; for &gt;26h ago" duration="1"/>
10+
<testCase name="humanizeTime returns &apos;a few seconds from now&apos; for &lt;15s ahead" duration="24"/>
11+
<testCase name="humanizeTime returns &apos;about a minute from now&apos; for &lt;75s ahead" duration="1"/>
12+
<testCase name="humanizeTime returns &apos;20 minutes from now&apos; for &lt;45min ahead" duration="1"/>
13+
<testCase name="humanizeTime returns &apos;about an hour from now&apos; for &lt;75min ahead" duration="2"/>
14+
<testCase name="humanizeTime returns &apos;5 hours from now&apos; for &lt;23h ahead" duration="0"/>
15+
<testCase name="humanizeTime returns &apos;about a day from now&apos; for &lt;26h ahead" duration="1"/>
16+
<testCase name="humanizeTime returns &apos;3 days from now&apos; for &gt;26h ahead" duration="0"/>
17+
</file>
18+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/utils/numbers.test.ts">
19+
<testCase name="numbers defaultNumber should use the default when the input is not a number" duration="13"/>
20+
<testCase name="numbers defaultNumber should use the number when it is a number" duration="0"/>
21+
<testCase name="numbers clamp should clamp high numbers" duration="1"/>
22+
<testCase name="numbers clamp should clamp low numbers" duration="0"/>
23+
<testCase name="numbers clamp should not clamp numbers in range" duration="1"/>
24+
<testCase name="numbers clamp should clamp floats" duration="1"/>
25+
<testCase name="numbers sum should sum" duration="1"/>
26+
<testCase name="numbers percentageWithin should work within 0-100" duration="0"/>
27+
<testCase name="numbers percentageWithin should work within 0-100 when pct &gt; 1" duration="0"/>
28+
<testCase name="numbers percentageWithin should work within 0-100 when pct &lt; 0" duration="0"/>
29+
<testCase name="numbers percentageWithin should work with ranges other than 0-100" duration="0"/>
30+
<testCase name="numbers percentageWithin should work with ranges other than 0-100 when pct &gt; 1" duration="0"/>
31+
<testCase name="numbers percentageWithin should work with ranges other than 0-100 when pct &lt; 0" duration="8"/>
32+
<testCase name="numbers percentageWithin should work with floats" duration="1"/>
33+
<testCase name="numbers percentageOf should work within 0-100" duration="13"/>
34+
<testCase name="numbers percentageOf should work within 0-100 when val &gt; 100" duration="9"/>
35+
<testCase name="numbers percentageOf should work within 0-100 when val &lt; 0" duration="0"/>
36+
<testCase name="numbers percentageOf should work with ranges other than 0-100" duration="0"/>
37+
<testCase name="numbers percentageOf should work with ranges other than 0-100 when val &gt; 100" duration="14"/>
38+
<testCase name="numbers percentageOf should work with ranges other than 0-100 when val &lt; 0" duration="0"/>
39+
<testCase name="numbers percentageOf should work with floats" duration="0"/>
40+
<testCase name="numbers percentageOf should return 0 for values that cause a division by zero" duration="0"/>
41+
</file>
42+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/utils/i18n.test.ts">
43+
<testCase name="i18n utils should wrap registerTranslations" duration="9"/>
44+
<testCase name="i18n utils should wrap setMissingEntryGenerator" duration="1"/>
45+
<testCase name="i18n utils should wrap getLocale" duration="1"/>
46+
<testCase name="i18n utils should wrap setLocale" duration="1"/>
47+
</file>
48+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/viewmodel/tests/Disposables.test.ts">
49+
<testCase name="Disposable isDisposed is true after dispose() is called" duration="9"/>
50+
<testCase name="Disposable dispose() calls the correct disposing function" duration="1"/>
51+
<testCase name="Disposable Throws error if acting on already disposed disposables" duration="16"/>
52+
<testCase name="Disposable Removes tracked event listeners on dispose" duration="1"/>
53+
</file>
54+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/viewmodel/tests/Snapshot.test.ts">
55+
<testCase name="Snapshot should accept an initial value" duration="10"/>
56+
<testCase name="Snapshot should call emit callback when state changes" duration="1"/>
57+
<testCase name="Snapshot should swap out entire snapshot on set call" duration="0"/>
58+
<testCase name="Snapshot should merge partial snapshot on merge call" duration="1"/>
59+
</file>
60+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/hooks/useListKeyboardNavigation.test.ts">
61+
<testCase name="useListKeyDown should handle Enter key to click active element" duration="50"/>
62+
<testCase name="useListKeyDown should handle Space key to click active element" duration="7"/>
63+
<testCase name="useListKeyDown should handle ArrowDown to focus the 1nth element" duration="3"/>
64+
<testCase name="useListKeyDown should handle ArrowUp to focus the 1nth element" duration="4"/>
65+
<testCase name="useListKeyDown should handle Home to focus the 0nth element" duration="4"/>
66+
<testCase name="useListKeyDown should handle End to focus the 2nth element" duration="6"/>
67+
<testCase name="useListKeyDown should not handle ArrowDown when active element is not in list" duration="5"/>
68+
<testCase name="useListKeyDown should not handle ArrowUp when active element is not in list" duration="2"/>
69+
<testCase name="useListKeyDown should not prevent default for unhandled keys" duration="2"/>
70+
<testCase name="useListKeyDown should focus the first item if list itself is focused" duration="11"/>
71+
<testCase name="useListKeyDown should focus the selected item if list itself is focused" duration="3"/>
72+
</file>
73+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/audio/Clock/Clock.test.tsx">
74+
<testCase name="Clock renders the clock" duration="46"/>
75+
<testCase name="Clock renders the clock with a lot of seconds" duration="11"/>
76+
</file>
77+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/pill-input/PillInput/PillInput.test.tsx">
78+
<testCase name="PillInput renders the pill input" duration="106"/>
79+
<testCase name="PillInput renders only the input without children" duration="24"/>
80+
<testCase name="PillInput calls onRemoveChildren when backspace is pressed and input is empty" duration="224"/>
81+
</file>
82+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/pill-input/Pill/Pill.test.tsx">
83+
<testCase name="Pill renders the pill" duration="116"/>
84+
<testCase name="Pill renders the pill without close button" duration="14"/>
85+
</file>
86+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/composer/Banner/Banner.test.tsx">
87+
<testCase name="AvatarWithDetails renders a default banner" duration="108"/>
88+
<testCase name="AvatarWithDetails renders a info banner" duration="16"/>
89+
<testCase name="AvatarWithDetails renders a success banner" duration="18"/>
90+
<testCase name="AvatarWithDetails renders a critical banner" duration="10"/>
91+
<testCase name="AvatarWithDetails renders a banner with an action" duration="20"/>
92+
<testCase name="AvatarWithDetails renders a banner with an avatar iamge" duration="46"/>
93+
</file>
94+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.test.tsx">
95+
<testCase name="AudioPlayerView renders the audio player in default state" duration="334"/>
96+
<testCase name="AudioPlayerView renders the audio player without media name" duration="70"/>
97+
<testCase name="AudioPlayerView renders the audio player without size" duration="72"/>
98+
<testCase name="AudioPlayerView renders the audio player in error state" duration="76"/>
99+
<testCase name="AudioPlayerView should attach vm methods" duration="437"/>
100+
</file>
101+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/rich-list/RichItem/RichItem.test.tsx">
102+
<testCase name="RichItem renders the item in default state" duration="269"/>
103+
<testCase name="RichItem renders the item in selected state" duration="33"/>
104+
<testCase name="RichItem renders the item without timestamp" duration="11"/>
105+
</file>
106+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/avatar/AvatarWithDetails/AvatarWithDetails.test.tsx">
107+
<testCase name="AvatarWithDetails renders a textual event" duration="27"/>
108+
</file>
109+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.test.tsx">
110+
<testCase name="PlayPauseButton renders the button in default state" duration="350"/>
111+
<testCase name="PlayPauseButton renders the button in playing state" duration="77"/>
112+
<testCase name="PlayPauseButton calls togglePlay when clicked" duration="1045"/>
113+
</file>
114+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/rich-list/RichList/RichList.test.tsx">
115+
<testCase name="RichItem renders the list" duration="83"/>
116+
<testCase name="RichItem renders the list with isEmpty=true" duration="7"/>
117+
</file>
118+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/audio/SeekBar/SeekBar.test.tsx">
119+
<testCase name="Seekbar renders the clock" duration="14"/>
120+
</file>
121+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/event-tiles/TextualEventView/TextualEventView.test.tsx">
122+
<testCase name="TextualEventView renders a textual event" duration="53"/>
123+
</file>
124+
<file path="/Users/skyezer/Work/element-hq/element-web/packages/shared-components/src/message-body/MediaBody/MediaBody.test.tsx">
125+
<testCase name="MediaBody renders the media body" duration="26"/>
126+
</file>
127+
</testExecutions>

packages/shared-components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"test:storybook:update": "playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot"
4747
},
4848
"dependencies": {
49+
"@vector-im/compound-design-tokens": "^6.3.0",
4950
"classnames": "^2.5.1",
5051
"counterpart": "^0.18.6",
5152
"lodash": "^4.17.21",
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright (c) 2025 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
:root {
9+
--cpd-color-gradient-critical-linear: linear-gradient(
10+
180deg,
11+
var(--cpd-color-alpha-red-500) 0%,
12+
var(--cpd-color-alpha-red-400) 20%,
13+
var(--cpd-color-alpha-red-300) 40%,
14+
var(--cpd-color-alpha-red-200) 60%,
15+
var(--cpd-color-alpha-red-100) 80%,
16+
var(--cpd-color-transparent) 100%
17+
);
18+
}
19+
20+
.banner {
21+
container-type: inline-size;
22+
container-name: banner;
23+
display: flex;
24+
align-items: center;
25+
justify-content: start;
26+
gap: var(--cpd-space-3x);
27+
padding: var(--cpd-space-4x);
28+
29+
border-top: 1px solid var(--cpd-color-gray-400);
30+
31+
white-space: nowrap;
32+
}
33+
34+
.banner[data-type="success"] {
35+
background: var(--cpd-color-gradient-subtle-linear);
36+
border-color: var(--cpd-color-green-900);
37+
}
38+
39+
.banner[data-type="critical"] {
40+
background: var(--cpd-color-gradient-critical-linear);
41+
border-color: var(--cpd-color-border-critical-primary);
42+
}
43+
44+
.banner[data-type="info"] {
45+
background: var(--cpd-color-gradient-info-linear);
46+
border-color: var(--cpd-color-blue-900);
47+
}
48+
49+
.banner[data-type="info"] :is(svg) {
50+
color: var(--cpd-color-blue-900);
51+
}
52+
53+
.banner[data-type="success"] :is(.content, svg) {
54+
color: var(--cpd-color-green-900);
55+
}
56+
57+
.banner[data-type="critical"] :is(.content, svg) {
58+
color: var(--cpd-color-red-900);
59+
}
60+
61+
.banner p {
62+
margin: 0;
63+
}
64+
65+
.icon {
66+
/* lock icon dimensions */
67+
min-width: 32px;
68+
min-height: 32px;
69+
max-width: 32px;
70+
max-height: 32px;
71+
72+
margin: 4px;
73+
74+
/* centre svg icons, as they are not full width */
75+
flex: 0;
76+
display: flex;
77+
align-items: center;
78+
justify-content: center;
79+
}
80+
81+
.icon img {
82+
border-radius: 50%;
83+
}
84+
85+
.actions {
86+
margin-left: auto;
87+
88+
flex: 0;
89+
display: flex;
90+
flex-direction: row;
91+
gap: var(--cpd-space-1x);
92+
align-self: center;
93+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright (c) 2025 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import React from "react";
9+
import { fn } from "storybook/test";
10+
import { type Meta, type StoryObj } from "@storybook/react-vite";
11+
import { Button } from "@vector-im/compound-web";
12+
13+
import { Banner } from "./Banner";
14+
import { _t } from "../../utils/i18n";
15+
16+
const meta = {
17+
title: "room/Banner",
18+
component: Banner,
19+
tags: ["autodocs"],
20+
args: {
21+
children: <p>Hello! This is a status banner.</p>,
22+
onClose: fn(),
23+
},
24+
} satisfies Meta<typeof Banner>;
25+
26+
export default meta;
27+
type Story = StoryObj<typeof meta>;
28+
29+
export const Default: Story = {};
30+
export const Info: Story = {
31+
args: {
32+
type: "info",
33+
},
34+
};
35+
export const Success: Story = {
36+
args: {
37+
type: "success",
38+
},
39+
};
40+
export const Critical: Story = {
41+
args: {
42+
type: "critical",
43+
},
44+
};
45+
export const WithAction: Story = {
46+
args: {
47+
children: (
48+
<p>
49+
{_t(
50+
"encryption|pinned_identity_changed",
51+
{ displayName: "Alice", userId: "@alice:example.org" },
52+
{
53+
a: (sub) => <a href="https://example.org">{sub}</a>,
54+
b: (sub) => <b>{sub}</b>,
55+
},
56+
)}
57+
</p>
58+
),
59+
actions: <Button kind="primary">{_t("encryption|withdraw_verification_action")}</Button>,
60+
},
61+
};
62+
63+
export const WithAvatarImage: Story = {
64+
args: {
65+
avatar: <img alt="Example" src="https://picsum.photos/32/32" />,
66+
},
67+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from "react";
2+
import { composeStories } from "@storybook/react-vite";
3+
4+
import * as stories from "./Banner.stories.tsx";
5+
import { render } from "jest-matrix-react";
6+
7+
const { Default, Info, Success, WithAction, WithAvatarImage, Critical } = composeStories(stories);
8+
9+
describe("AvatarWithDetails", () => {
10+
it("renders a default banner", () => {
11+
const { container } = render(<Default />);
12+
expect(container).toMatchSnapshot();
13+
});
14+
it("renders a info banner", () => {
15+
const { container } = render(<Info />);
16+
expect(container).toMatchSnapshot();
17+
});
18+
it("renders a success banner", () => {
19+
const { container } = render(<Success />);
20+
expect(container).toMatchSnapshot();
21+
});
22+
it("renders a critical banner", () => {
23+
const { container } = render(<Critical />);
24+
expect(container).toMatchSnapshot();
25+
});
26+
it("renders a banner with an action", () => {
27+
const { container } = render(<WithAction />);
28+
expect(container).toMatchSnapshot();
29+
});
30+
it("renders a banner with an avatar iamge", () => {
31+
const { container } = render(<WithAvatarImage />);
32+
expect(container).toMatchSnapshot();
33+
});
34+
});

0 commit comments

Comments
 (0)