Make shared-components tiles render identically outside Element Web - # 1 (#33418)
* Apply event presentation attributes to timeline previews and errors * Make TextualEventView render the same in web and storybook * Make TileErrorView render the same in app/web and storybook * Updated snapshots * Make it possible to view shared components with default app/web base styling. * Adjust styling and add underline to pass tests * Fix Sonar issue * Rename base css to root css * Handle font styling correctly
@@ -7,12 +7,8 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_TextualEvent {
|
.mx_TextualEvent {
|
||||||
overflow-y: hidden;
|
|
||||||
line-height: normal;
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $accent;
|
color: $accent;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomView_searchResultsPanel & {
|
.mx_RoomView_searchResultsPanel & {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const LegacyCallEventFactory: Factory<FactoryProps & { callEventGrouper: LegacyC
|
|||||||
const CallEventFactory: Factory = (ref, props) => <CallEvent ref={ref} {...props} />;
|
const CallEventFactory: Factory = (ref, props) => <CallEvent ref={ref} {...props} />;
|
||||||
export const TextualEventFactory: Factory = (ref, props) => {
|
export const TextualEventFactory: Factory = (ref, props) => {
|
||||||
const vm = new TextualEventViewModel(props);
|
const vm = new TextualEventViewModel(props);
|
||||||
return <TextualEventView vm={vm} />;
|
return <TextualEventView vm={vm} className="mx_TextualEvent" />;
|
||||||
};
|
};
|
||||||
function EncryptionEventWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
|
function EncryptionEventWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
|
||||||
const cli = useMatrixClientContext();
|
const cli = useMatrixClientContext();
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Match the app/web shell baseline without importing app/web component styles.
|
||||||
|
* Shared component stories should inherit the same root typography and font
|
||||||
|
* feature settings as the real app, while still owning their component styling.
|
||||||
|
*/
|
||||||
|
@layer app-web {
|
||||||
|
:root[data-storybook-root-css="app-web"] {
|
||||||
|
[class^="cpd-theme"][class^="cpd-theme"] {
|
||||||
|
--cpd-font-family-sans:
|
||||||
|
"Inter", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", sans-serif,
|
||||||
|
"Noto Color Emoji";
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font: var(--cpd-font-body-md-regular) !important;
|
||||||
|
letter-spacing: var(--cpd-font-letter-spacing-body-md) !important;
|
||||||
|
font-feature-settings:
|
||||||
|
"kern" 1,
|
||||||
|
"liga" 1,
|
||||||
|
"calt" 1 !important;
|
||||||
|
|
||||||
|
background-color: var(--cpd-color-bg-canvas-default);
|
||||||
|
color: var(--cpd-color-text-primary);
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code {
|
||||||
|
font-family:
|
||||||
|
"Fira Code", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace, "Noto Color Emoji";
|
||||||
|
font-size: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Username color classes - these are defined in the main app's _common.pcss
|
||||||
|
but need to be available in Storybook for components that use colorClass */
|
||||||
|
.mx_Username_color1 {
|
||||||
|
color: var(--cpd-color-text-decorative-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Username_color2 {
|
||||||
|
color: var(--cpd-color-text-decorative-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Username_color3 {
|
||||||
|
color: var(--cpd-color-text-decorative-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Username_color4 {
|
||||||
|
color: var(--cpd-color-text-decorative-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Username_color5 {
|
||||||
|
color: var(--cpd-color-text-decorative-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Username_color6 {
|
||||||
|
color: var(--cpd-color-text-decorative-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,29 +8,3 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
.docs-story {
|
.docs-story {
|
||||||
background: var(--cpd-color-bg-canvas-default);
|
background: var(--cpd-color-bg-canvas-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Username color classes - these are defined in the main app's _common.pcss
|
|
||||||
but need to be available in Storybook for components that use colorClass */
|
|
||||||
.mx_Username_color1 {
|
|
||||||
color: var(--cpd-color-text-decorative-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_Username_color2 {
|
|
||||||
color: var(--cpd-color-text-decorative-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_Username_color3 {
|
|
||||||
color: var(--cpd-color-text-decorative-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_Username_color4 {
|
|
||||||
color: var(--cpd-color-text-decorative-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_Username_color5 {
|
|
||||||
color: var(--cpd-color-text-decorative-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_Username_color6 {
|
|
||||||
color: var(--cpd-color-text-decorative-6);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import "@fontsource/inter/600.css";
|
|||||||
import "@fontsource/inter/700.css";
|
import "@fontsource/inter/700.css";
|
||||||
|
|
||||||
import "./compound.css";
|
import "./compound.css";
|
||||||
|
import "./app-web-root.css";
|
||||||
import "./preview.css";
|
import "./preview.css";
|
||||||
import React, { useLayoutEffect } from "react";
|
import React, { useLayoutEffect } from "react";
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
@@ -47,7 +48,7 @@ export const globalTypes = {
|
|||||||
icon: "component",
|
icon: "component",
|
||||||
title: "Event layout",
|
title: "Event layout",
|
||||||
items: [
|
items: [
|
||||||
{ title: "Group", value: "group" },
|
{ title: "Modern", value: "group" },
|
||||||
{ title: "Bubble", value: "bubble" },
|
{ title: "Bubble", value: "bubble" },
|
||||||
{ title: "IRC", value: "irc" },
|
{ title: "IRC", value: "irc" },
|
||||||
],
|
],
|
||||||
@@ -65,16 +66,47 @@ export const globalTypes = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
rootCss: {
|
||||||
|
name: "Root CSS",
|
||||||
|
description: "Global root CSS for component previews",
|
||||||
|
toolbar: {
|
||||||
|
icon: "paintbrush",
|
||||||
|
title: "Root CSS",
|
||||||
|
items: [
|
||||||
|
{ title: "Default", value: "storybook" },
|
||||||
|
{ title: "Element Web", value: "app-web" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
initialGlobals: {
|
initialGlobals: {
|
||||||
theme: "system",
|
theme: "light",
|
||||||
language: "en",
|
language: "en",
|
||||||
eventLayout: "group",
|
eventLayout: "group",
|
||||||
eventDensity: "default",
|
eventDensity: "default",
|
||||||
|
rootCss: "storybook",
|
||||||
},
|
},
|
||||||
} satisfies ArgTypes;
|
} satisfies ArgTypes;
|
||||||
|
|
||||||
const allThemesClasses = globalTypes.theme.toolbar.items.map(({ value }) => `cpd-theme-${value}`);
|
const allThemesClasses = globalTypes.theme.toolbar.items.map(({ value }) => `cpd-theme-${value}`);
|
||||||
|
|
||||||
|
const RootCssSwitcher: React.FC<{
|
||||||
|
rootCss: string;
|
||||||
|
}> = ({ rootCss }) => {
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (rootCss === "app-web") {
|
||||||
|
document.documentElement.dataset.storybookRootCss = rootCss;
|
||||||
|
} else {
|
||||||
|
delete document.documentElement.dataset.storybookRootCss;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
delete document.documentElement.dataset.storybookRootCss;
|
||||||
|
};
|
||||||
|
}, [rootCss]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const ThemeSwitcher: React.FC<{
|
const ThemeSwitcher: React.FC<{
|
||||||
theme: string;
|
theme: string;
|
||||||
}> = ({ theme }) => {
|
}> = ({ theme }) => {
|
||||||
@@ -89,6 +121,15 @@ const ThemeSwitcher: React.FC<{
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const withRootCss: Decorator = (Story, context) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RootCssSwitcher rootCss={context.globals.rootCss} />
|
||||||
|
<Story />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const withThemeProvider: Decorator = (Story, context) => {
|
const withThemeProvider: Decorator = (Story, context) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -134,12 +175,13 @@ const withEventPresentationProvider: Decorator = (Story, context) => {
|
|||||||
const preview = {
|
const preview = {
|
||||||
tags: ["autodocs", "snapshot"],
|
tags: ["autodocs", "snapshot"],
|
||||||
initialGlobals: {
|
initialGlobals: {
|
||||||
theme: "system",
|
rootCss: "storybook",
|
||||||
|
theme: "light",
|
||||||
language: "en",
|
language: "en",
|
||||||
eventLayout: "group",
|
eventLayout: "group",
|
||||||
eventDensity: "default",
|
eventDensity: "default",
|
||||||
},
|
},
|
||||||
decorators: [withThemeProvider, withEventPresentationProvider, withTooltipProvider, withI18nProvider],
|
decorators: [withRootCss, withThemeProvider, withEventPresentationProvider, withTooltipProvider, withI18nProvider],
|
||||||
parameters: {
|
parameters: {
|
||||||
options: {
|
options: {
|
||||||
storySort: {
|
storySort: {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 20 KiB |
@@ -37,3 +37,20 @@ export const EventPresentationProvider = EventPresentationContext.Provider;
|
|||||||
export function useEventPresentation(): EventPresentation {
|
export function useEventPresentation(): EventPresentation {
|
||||||
return useContext(EventPresentationContext);
|
return useContext(EventPresentationContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the current event presentation settings attributes.
|
||||||
|
*
|
||||||
|
* "data-event-layout": "group" | "bubble" | "irc"
|
||||||
|
* "data-event-density": "default" | "compact"
|
||||||
|
*/
|
||||||
|
export function useEventPresentationAttributes(): {
|
||||||
|
"data-event-layout": EventLayout;
|
||||||
|
"data-event-density": EventDensity;
|
||||||
|
} {
|
||||||
|
const { layout, density } = useEventPresentation();
|
||||||
|
|
||||||
|
return {
|
||||||
|
"data-event-layout": layout,
|
||||||
|
"data-event-density": density,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.textualEvent {
|
||||||
|
font-size: var(--cpd-font-size-body-sm);
|
||||||
|
line-height: normal;
|
||||||
|
|
||||||
|
overflow-y: hidden;
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
|
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--cpd-color-text-action-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.textualEvent[data-event-layout="irc"] {
|
||||||
|
padding: 1px 0; /* add a 1px padding top and bottom because our larger emoji font otherwise gets cropped by anti-zalgo */
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1.125rem; /* $font-18px equivalent of the legacy --irc-line-height */
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { type Meta, type StoryObj } from "@storybook/react-vite";
|
import { type Meta, type StoryObj } from "@storybook/react-vite";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import { TextualEventView as TextualEventComponent } from "./TextualEventView";
|
import { TextualEventView as TextualEventComponent } from "./TextualEventView";
|
||||||
import { MockViewModel } from "../../../../../core/viewmodel/MockViewModel";
|
import { MockViewModel } from "../../../../../core/viewmodel/MockViewModel";
|
||||||
@@ -23,3 +24,16 @@ export default meta;
|
|||||||
type Story = StoryObj<typeof meta>;
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
export const Default: Story = {};
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
export const WithLink: Story = {
|
||||||
|
args: {
|
||||||
|
vm: new MockViewModel({
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<span>Dummy [🤒] textual event text </span>
|
||||||
|
<a href="~">with link</a>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -12,11 +12,16 @@ import { describe, it, expect } from "vitest";
|
|||||||
|
|
||||||
import * as stories from "./TextualEventView.stories.tsx";
|
import * as stories from "./TextualEventView.stories.tsx";
|
||||||
|
|
||||||
const { Default } = composeStories(stories);
|
const { Default, WithLink } = composeStories(stories);
|
||||||
|
|
||||||
describe("TextualEventView", () => {
|
describe("TextualEventView", () => {
|
||||||
it("renders a textual event", () => {
|
it("renders a textual event", () => {
|
||||||
const { container } = render(<Default />);
|
const { container } = render(<Default />);
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders a textual event with link", () => {
|
||||||
|
const { container } = render(<WithLink />);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,15 +6,22 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type ReactNode, type JSX } from "react";
|
import React, { type ReactNode, type JSX } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { type ViewModel, useViewModel } from "../../../../../core/viewmodel";
|
import { type ViewModel, useViewModel } from "../../../../../core/viewmodel";
|
||||||
|
import styles from "./TextualEventView.module.css";
|
||||||
|
import { useEventPresentationAttributes } from "../../../EventPresentation/EventPresentationContext";
|
||||||
|
|
||||||
export type TextualEventViewSnapshot = {
|
export type TextualEventViewSnapshot = {
|
||||||
content: string | ReactNode;
|
content: string | ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
/** The view model for the tile error fallback. */
|
||||||
vm: ViewModel<TextualEventViewSnapshot>;
|
vm: ViewModel<TextualEventViewSnapshot>;
|
||||||
|
|
||||||
|
/** Optional host-level class names. */
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,7 +30,12 @@ export interface Props {
|
|||||||
* This view is used for simple informational timeline entries where the
|
* This view is used for simple informational timeline entries where the
|
||||||
* content is already prepared by the view model.
|
* content is already prepared by the view model.
|
||||||
*/
|
*/
|
||||||
export function TextualEventView({ vm }: Props): JSX.Element {
|
export function TextualEventView({ vm, className }: Readonly<Props>): JSX.Element {
|
||||||
|
const eventPresentationAttributes = useEventPresentationAttributes();
|
||||||
const snapshot = useViewModel(vm);
|
const snapshot = useViewModel(vm);
|
||||||
return <div className="mx_TextualEvent">{snapshot.content}</div>;
|
return (
|
||||||
|
<div className={classNames(styles.textualEvent, className)} {...eventPresentationAttributes}>
|
||||||
|
{snapshot.content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,30 @@
|
|||||||
exports[`TextualEventView > renders a textual event 1`] = `
|
exports[`TextualEventView > renders a textual event 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="mx_TextualEvent"
|
class="TextualEventView-module_textualEvent"
|
||||||
|
data-event-density="default"
|
||||||
|
data-event-layout="group"
|
||||||
>
|
>
|
||||||
Dummy textual event text
|
Dummy textual event text
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`TextualEventView > renders a textual event with link 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="TextualEventView-module_textualEvent"
|
||||||
|
data-event-density="default"
|
||||||
|
data-event-layout="group"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Dummy [🤒] textual event text
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="~"
|
||||||
|
>
|
||||||
|
with link
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|||||||
@@ -1,38 +1,42 @@
|
|||||||
.tileErrorView {
|
.tileErrorView {
|
||||||
color: var(--cpd-color-text-critical-primary);
|
font-size: var(--cpd-font-size-body-sm);
|
||||||
|
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
list-style: none;
|
list-style: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
.line {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--cpd-space-2x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
padding: var(--cpd-space-1x) var(--cpd-space-4x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewSourceButton {
|
||||||
|
appearance: none;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
color: var(--cpd-color-text-action-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewSourceButton:focus-visible {
|
||||||
|
outline: 2px solid var(--cpd-color-border-focused);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: var(--cpd-space-1x);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.line {
|
.tileErrorView[data-event-layout="bubble"] {
|
||||||
display: flex;
|
.line {
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
align-items: center;
|
}
|
||||||
justify-content: center;
|
|
||||||
gap: var(--cpd-space-2x);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble .line {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
padding: var(--cpd-space-1x) var(--cpd-space-4x);
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewSourceButton {
|
|
||||||
appearance: none;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
background: none;
|
|
||||||
color: var(--cpd-color-text-action-primary);
|
|
||||||
cursor: pointer;
|
|
||||||
font: inherit;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewSourceButton:focus-visible {
|
|
||||||
outline: 2px solid var(--cpd-color-border-focused);
|
|
||||||
outline-offset: 2px;
|
|
||||||
border-radius: var(--cpd-space-1x);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { fn } from "storybook/test";
|
|||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import { withViewDocs } from "../../../../../../.storybook/withViewDocs";
|
import { withViewDocs } from "../../../../../../.storybook/withViewDocs";
|
||||||
import { useMockedViewModel } from "../../../../../core/viewmodel";
|
import { useMockedViewModel } from "../../../../../core/viewmodel";
|
||||||
import { EventPresentationProvider } from "../../../EventPresentation";
|
|
||||||
import { TileErrorView, type TileErrorViewActions, type TileErrorViewSnapshot } from "./TileErrorView";
|
import { TileErrorView, type TileErrorViewActions, type TileErrorViewSnapshot } from "./TileErrorView";
|
||||||
|
|
||||||
type WrapperProps = TileErrorViewSnapshot &
|
type WrapperProps = TileErrorViewSnapshot &
|
||||||
@@ -59,13 +58,9 @@ type Story = StoryObj<typeof meta>;
|
|||||||
export const Default: Story = {};
|
export const Default: Story = {};
|
||||||
|
|
||||||
export const BubbleLayout: Story = {
|
export const BubbleLayout: Story = {
|
||||||
decorators: [
|
globals: {
|
||||||
(Story): JSX.Element => (
|
eventLayout: "bubble",
|
||||||
<EventPresentationProvider value={{ layout: "bubble", density: "default" }}>
|
},
|
||||||
<Story />
|
|
||||||
</EventPresentationProvider>
|
|
||||||
),
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithoutActions: Story = {
|
export const WithoutActions: Story = {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import classNames from "classnames";
|
|||||||
import { Button } from "@vector-im/compound-web";
|
import { Button } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { type ViewModel, useViewModel } from "../../../../../core/viewmodel";
|
import { type ViewModel, useViewModel } from "../../../../../core/viewmodel";
|
||||||
import { useEventPresentation } from "../../../EventPresentation";
|
|
||||||
import styles from "./TileErrorView.module.css";
|
import styles from "./TileErrorView.module.css";
|
||||||
|
import { useEventPresentationAttributes } from "../../../EventPresentation/EventPresentationContext";
|
||||||
|
|
||||||
/** Snapshot data for rendering an event tile error fallback. */
|
/** Snapshot data for rendering an event tile error fallback. */
|
||||||
export interface TileErrorViewSnapshot {
|
export interface TileErrorViewSnapshot {
|
||||||
@@ -55,14 +55,11 @@ interface TileErrorViewProps {
|
|||||||
* actions when their labels are provided.
|
* actions when their labels are provided.
|
||||||
*/
|
*/
|
||||||
export function TileErrorView({ vm, className }: Readonly<TileErrorViewProps>): JSX.Element {
|
export function TileErrorView({ vm, className }: Readonly<TileErrorViewProps>): JSX.Element {
|
||||||
const { layout } = useEventPresentation();
|
const eventPresentationAttributes = useEventPresentationAttributes();
|
||||||
const { message, eventType, bugReportCtaLabel, viewSourceCtaLabel } = useViewModel(vm);
|
const { message, eventType, bugReportCtaLabel, viewSourceCtaLabel } = useViewModel(vm);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li className={classNames(styles.tileErrorView, className)} {...eventPresentationAttributes}>
|
||||||
className={classNames(styles.tileErrorView, className, { [styles.bubble]: layout === "bubble" })}
|
|
||||||
data-layout={layout}
|
|
||||||
>
|
|
||||||
<div className={styles.line} role="status">
|
<div className={styles.line} role="status">
|
||||||
<span className={styles.message}>
|
<span className={styles.message}>
|
||||||
{message}
|
{message}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ exports[`TileErrorView > renders the bubble layout variant 1`] = `
|
|||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
class="TileErrorView-module_tileErrorView TileErrorView-module_bubble"
|
class="TileErrorView-module_tileErrorView"
|
||||||
data-layout="bubble"
|
data-event-density="default"
|
||||||
|
data-event-layout="group"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="TileErrorView-module_line"
|
class="TileErrorView-module_line"
|
||||||
@@ -43,7 +44,8 @@ exports[`TileErrorView > renders the default tile error state 1`] = `
|
|||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
class="TileErrorView-module_tileErrorView"
|
class="TileErrorView-module_tileErrorView"
|
||||||
data-layout="group"
|
data-event-density="default"
|
||||||
|
data-event-layout="group"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="TileErrorView-module_line"
|
class="TileErrorView-module_line"
|
||||||
|
|||||||
@@ -5,29 +5,31 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.previewGroup {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--cpd-space-4x);
|
|
||||||
margin-bottom: var(--cpd-space-4x);
|
|
||||||
|
|
||||||
&.compactLayout {
|
|
||||||
gap: var(--cpd-space-2x);
|
|
||||||
margin-bottom: var(--cpd-space-2x);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggleButton[data-kind="tertiary"] {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--cpd-color-icon-accent-primary);
|
|
||||||
font-weight: var(--cpd-font-weight-regular);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
margin-top: var(--cpd-space-4x);
|
margin-top: var(--cpd-space-4x);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
|
.previewGroup {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-4x);
|
||||||
|
margin-bottom: var(--cpd-space-4x);
|
||||||
|
|
||||||
|
.toggleButton[data-kind="tertiary"] {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--cpd-color-icon-accent-primary);
|
||||||
|
font-weight: var(--cpd-font-weight-regular);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper[data-event-density="compact"] {
|
||||||
|
.previewGroup {
|
||||||
|
gap: var(--cpd-space-2x);
|
||||||
|
margin-bottom: var(--cpd-space-2x);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
import { useMockedViewModel } from "../../../../core/viewmodel";
|
import { useMockedViewModel } from "../../../../core/viewmodel";
|
||||||
import { LinkedTextContext } from "../../../../core/utils/LinkedText";
|
import { LinkedTextContext } from "../../../../core/utils/LinkedText";
|
||||||
import { withViewDocs } from "../../../../../.storybook/withViewDocs";
|
import { withViewDocs } from "../../../../../.storybook/withViewDocs";
|
||||||
import { EventPresentationProvider } from "../../EventPresentation";
|
|
||||||
|
|
||||||
type UrlPreviewGroupViewProps = UrlPreviewGroupViewSnapshot & UrlPreviewGroupViewActions;
|
type UrlPreviewGroupViewProps = UrlPreviewGroupViewSnapshot & UrlPreviewGroupViewActions;
|
||||||
|
|
||||||
@@ -142,10 +141,6 @@ export const WithCompactView = Template.bind({});
|
|||||||
WithCompactView.args = {
|
WithCompactView.args = {
|
||||||
...MultiplePreviewsVisible.args,
|
...MultiplePreviewsVisible.args,
|
||||||
};
|
};
|
||||||
WithCompactView.decorators = [
|
WithCompactView.globals = {
|
||||||
(Story): JSX.Element => (
|
eventDensity: "compact",
|
||||||
<EventPresentationProvider value={{ layout: "group", density: "compact" }}>
|
};
|
||||||
<Story />
|
|
||||||
</EventPresentationProvider>
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import classNames from "classnames";
|
|||||||
|
|
||||||
import { useViewModel, type ViewModel } from "../../../../core/viewmodel";
|
import { useViewModel, type ViewModel } from "../../../../core/viewmodel";
|
||||||
import { useI18n } from "../../../../core/i18n/i18nContext";
|
import { useI18n } from "../../../../core/i18n/i18nContext";
|
||||||
import { useEventPresentation } from "../../EventPresentation";
|
|
||||||
import type { UrlPreview } from "./types";
|
import type { UrlPreview } from "./types";
|
||||||
import { LinkPreview } from "./LinkPreview";
|
import { LinkPreview } from "./LinkPreview";
|
||||||
import styles from "./UrlPreviewGroupView.module.css";
|
import styles from "./UrlPreviewGroupView.module.css";
|
||||||
|
import { useEventPresentationAttributes } from "../../EventPresentation/EventPresentationContext";
|
||||||
|
|
||||||
/** Snapshot data for rendering URL previews attached to an event. */
|
/** Snapshot data for rendering URL previews attached to an event. */
|
||||||
export interface UrlPreviewGroupViewSnapshot {
|
export interface UrlPreviewGroupViewSnapshot {
|
||||||
@@ -62,7 +62,7 @@ export type UrlPreviewGroupViewModel = ViewModel<UrlPreviewGroupViewSnapshot, Ur
|
|||||||
*/
|
*/
|
||||||
export function UrlPreviewGroupView({ vm, className }: UrlPreviewGroupViewProps): JSX.Element | null {
|
export function UrlPreviewGroupView({ vm, className }: UrlPreviewGroupViewProps): JSX.Element | null {
|
||||||
const { translate: _t } = useI18n();
|
const { translate: _t } = useI18n();
|
||||||
const { density } = useEventPresentation();
|
const eventPresentationAttributes = useEventPresentationAttributes();
|
||||||
const { previews, totalPreviewCount, previewsLimited, overPreviewLimit } = useViewModel(vm);
|
const { previews, totalPreviewCount, previewsLimited, overPreviewLimit } = useViewModel(vm);
|
||||||
if (previews.length === 0) {
|
if (previews.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -80,8 +80,8 @@ export function UrlPreviewGroupView({ vm, className }: UrlPreviewGroupViewProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(className, styles.wrapper)}>
|
<div className={classNames(className, styles.wrapper)} {...eventPresentationAttributes}>
|
||||||
<div className={classNames(styles.previewGroup, density === "compact" && styles.compactLayout)}>
|
<div className={styles.previewGroup}>
|
||||||
{previews.map((preview) => (
|
{previews.map((preview) => (
|
||||||
<LinkPreview key={preview.link} onImageClick={() => vm.onImageClick(preview)} {...preview} />
|
<LinkPreview key={preview.link} onImageClick={() => vm.onImageClick(preview)} {...preview} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ exports[`UrlPreviewGroupView > renders a single preview 1`] = `
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="UrlPreviewGroupView-module_wrapper"
|
class="UrlPreviewGroupView-module_wrapper"
|
||||||
|
data-event-density="default"
|
||||||
|
data-event-layout="group"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="UrlPreviewGroupView-module_previewGroup"
|
class="UrlPreviewGroupView-module_previewGroup"
|
||||||
@@ -82,6 +84,8 @@ exports[`UrlPreviewGroupView > renders multiple previews 1`] = `
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="UrlPreviewGroupView-module_wrapper"
|
class="UrlPreviewGroupView-module_wrapper"
|
||||||
|
data-event-density="default"
|
||||||
|
data-event-layout="group"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="UrlPreviewGroupView-module_previewGroup"
|
class="UrlPreviewGroupView-module_previewGroup"
|
||||||
@@ -249,6 +253,8 @@ exports[`UrlPreviewGroupView > renders multiple previews which are hidden 1`] =
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="UrlPreviewGroupView-module_wrapper"
|
class="UrlPreviewGroupView-module_wrapper"
|
||||||
|
data-event-density="default"
|
||||||
|
data-event-layout="group"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="UrlPreviewGroupView-module_previewGroup"
|
class="UrlPreviewGroupView-module_previewGroup"
|
||||||
@@ -336,9 +342,11 @@ exports[`UrlPreviewGroupView > renders with compact density 1`] = `
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="UrlPreviewGroupView-module_wrapper"
|
class="UrlPreviewGroupView-module_wrapper"
|
||||||
|
data-event-density="default"
|
||||||
|
data-event-layout="group"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="UrlPreviewGroupView-module_previewGroup UrlPreviewGroupView-module_compactLayout"
|
class="UrlPreviewGroupView-module_previewGroup"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="LinkPreview-module_container"
|
class="LinkPreview-module_container"
|
||||||
|
|||||||