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 {
|
||||
overflow-y: hidden;
|
||||
line-height: normal;
|
||||
|
||||
a {
|
||||
color: $accent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_RoomView_searchResultsPanel & {
|
||||
|
||||
@@ -84,7 +84,7 @@ const LegacyCallEventFactory: Factory<FactoryProps & { callEventGrouper: LegacyC
|
||||
const CallEventFactory: Factory = (ref, props) => <CallEvent ref={ref} {...props} />;
|
||||
export const TextualEventFactory: Factory = (ref, props) => {
|
||||
const vm = new TextualEventViewModel(props);
|
||||
return <TextualEventView vm={vm} />;
|
||||
return <TextualEventView vm={vm} className="mx_TextualEvent" />;
|
||||
};
|
||||
function EncryptionEventWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
|
||||
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 {
|
||||
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 "./compound.css";
|
||||
import "./app-web-root.css";
|
||||
import "./preview.css";
|
||||
import React, { useLayoutEffect } from "react";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
@@ -47,7 +48,7 @@ export const globalTypes = {
|
||||
icon: "component",
|
||||
title: "Event layout",
|
||||
items: [
|
||||
{ title: "Group", value: "group" },
|
||||
{ title: "Modern", value: "group" },
|
||||
{ title: "Bubble", value: "bubble" },
|
||||
{ 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: {
|
||||
theme: "system",
|
||||
theme: "light",
|
||||
language: "en",
|
||||
eventLayout: "group",
|
||||
eventDensity: "default",
|
||||
rootCss: "storybook",
|
||||
},
|
||||
} satisfies ArgTypes;
|
||||
|
||||
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<{
|
||||
theme: string;
|
||||
}> = ({ theme }) => {
|
||||
@@ -89,6 +121,15 @@ const ThemeSwitcher: React.FC<{
|
||||
return null;
|
||||
};
|
||||
|
||||
const withRootCss: Decorator = (Story, context) => {
|
||||
return (
|
||||
<>
|
||||
<RootCssSwitcher rootCss={context.globals.rootCss} />
|
||||
<Story />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const withThemeProvider: Decorator = (Story, context) => {
|
||||
return (
|
||||
<>
|
||||
@@ -134,12 +175,13 @@ const withEventPresentationProvider: Decorator = (Story, context) => {
|
||||
const preview = {
|
||||
tags: ["autodocs", "snapshot"],
|
||||
initialGlobals: {
|
||||
theme: "system",
|
||||
rootCss: "storybook",
|
||||
theme: "light",
|
||||
language: "en",
|
||||
eventLayout: "group",
|
||||
eventDensity: "default",
|
||||
},
|
||||
decorators: [withThemeProvider, withEventPresentationProvider, withTooltipProvider, withI18nProvider],
|
||||
decorators: [withRootCss, withThemeProvider, withEventPresentationProvider, withTooltipProvider, withI18nProvider],
|
||||
parameters: {
|
||||
options: {
|
||||
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 {
|
||||
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 React from "react";
|
||||
|
||||
import { TextualEventView as TextualEventComponent } from "./TextualEventView";
|
||||
import { MockViewModel } from "../../../../../core/viewmodel/MockViewModel";
|
||||
@@ -23,3 +24,16 @@ export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
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";
|
||||
|
||||
const { Default } = composeStories(stories);
|
||||
const { Default, WithLink } = composeStories(stories);
|
||||
|
||||
describe("TextualEventView", () => {
|
||||
it("renders a textual event", () => {
|
||||
const { container } = render(<Default />);
|
||||
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 classNames from "classnames";
|
||||
|
||||
import { type ViewModel, useViewModel } from "../../../../../core/viewmodel";
|
||||
import styles from "./TextualEventView.module.css";
|
||||
import { useEventPresentationAttributes } from "../../../EventPresentation/EventPresentationContext";
|
||||
|
||||
export type TextualEventViewSnapshot = {
|
||||
content: string | ReactNode;
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
/** The view model for the tile error fallback. */
|
||||
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
|
||||
* 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);
|
||||
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`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_TextualEvent"
|
||||
class="TextualEventView-module_textualEvent"
|
||||
data-event-density="default"
|
||||
data-event-layout="group"
|
||||
>
|
||||
Dummy textual event text
|
||||
</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 {
|
||||
color: var(--cpd-color-text-critical-primary);
|
||||
font-size: var(--cpd-font-size-body-sm);
|
||||
|
||||
color: var(--cpd-color-text-secondary);
|
||||
list-style: none;
|
||||
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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
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);
|
||||
.tileErrorView[data-event-layout="bubble"] {
|
||||
.line {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import { fn } from "storybook/test";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { withViewDocs } from "../../../../../../.storybook/withViewDocs";
|
||||
import { useMockedViewModel } from "../../../../../core/viewmodel";
|
||||
import { EventPresentationProvider } from "../../../EventPresentation";
|
||||
import { TileErrorView, type TileErrorViewActions, type TileErrorViewSnapshot } from "./TileErrorView";
|
||||
|
||||
type WrapperProps = TileErrorViewSnapshot &
|
||||
@@ -59,13 +58,9 @@ type Story = StoryObj<typeof meta>;
|
||||
export const Default: Story = {};
|
||||
|
||||
export const BubbleLayout: Story = {
|
||||
decorators: [
|
||||
(Story): JSX.Element => (
|
||||
<EventPresentationProvider value={{ layout: "bubble", density: "default" }}>
|
||||
<Story />
|
||||
</EventPresentationProvider>
|
||||
),
|
||||
],
|
||||
globals: {
|
||||
eventLayout: "bubble",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutActions: Story = {
|
||||
|
||||
@@ -10,8 +10,8 @@ import classNames from "classnames";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
|
||||
import { type ViewModel, useViewModel } from "../../../../../core/viewmodel";
|
||||
import { useEventPresentation } from "../../../EventPresentation";
|
||||
import styles from "./TileErrorView.module.css";
|
||||
import { useEventPresentationAttributes } from "../../../EventPresentation/EventPresentationContext";
|
||||
|
||||
/** Snapshot data for rendering an event tile error fallback. */
|
||||
export interface TileErrorViewSnapshot {
|
||||
@@ -55,14 +55,11 @@ interface TileErrorViewProps {
|
||||
* actions when their labels are provided.
|
||||
*/
|
||||
export function TileErrorView({ vm, className }: Readonly<TileErrorViewProps>): JSX.Element {
|
||||
const { layout } = useEventPresentation();
|
||||
const eventPresentationAttributes = useEventPresentationAttributes();
|
||||
const { message, eventType, bugReportCtaLabel, viewSourceCtaLabel } = useViewModel(vm);
|
||||
|
||||
return (
|
||||
<li
|
||||
className={classNames(styles.tileErrorView, className, { [styles.bubble]: layout === "bubble" })}
|
||||
data-layout={layout}
|
||||
>
|
||||
<li className={classNames(styles.tileErrorView, className)} {...eventPresentationAttributes}>
|
||||
<div className={styles.line} role="status">
|
||||
<span className={styles.message}>
|
||||
{message}
|
||||
|
||||
@@ -4,8 +4,9 @@ exports[`TileErrorView > renders the bubble layout variant 1`] = `
|
||||
<div>
|
||||
<ul>
|
||||
<li
|
||||
class="TileErrorView-module_tileErrorView TileErrorView-module_bubble"
|
||||
data-layout="bubble"
|
||||
class="TileErrorView-module_tileErrorView"
|
||||
data-event-density="default"
|
||||
data-event-layout="group"
|
||||
>
|
||||
<div
|
||||
class="TileErrorView-module_line"
|
||||
@@ -43,7 +44,8 @@ exports[`TileErrorView > renders the default tile error state 1`] = `
|
||||
<ul>
|
||||
<li
|
||||
class="TileErrorView-module_tileErrorView"
|
||||
data-layout="group"
|
||||
data-event-density="default"
|
||||
data-event-layout="group"
|
||||
>
|
||||
<div
|
||||
class="TileErrorView-module_line"
|
||||
|
||||
@@ -5,29 +5,31 @@
|
||||
* 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 {
|
||||
margin-top: var(--cpd-space-4x);
|
||||
display: flex;
|
||||
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 { LinkedTextContext } from "../../../../core/utils/LinkedText";
|
||||
import { withViewDocs } from "../../../../../.storybook/withViewDocs";
|
||||
import { EventPresentationProvider } from "../../EventPresentation";
|
||||
|
||||
type UrlPreviewGroupViewProps = UrlPreviewGroupViewSnapshot & UrlPreviewGroupViewActions;
|
||||
|
||||
@@ -142,10 +141,6 @@ export const WithCompactView = Template.bind({});
|
||||
WithCompactView.args = {
|
||||
...MultiplePreviewsVisible.args,
|
||||
};
|
||||
WithCompactView.decorators = [
|
||||
(Story): JSX.Element => (
|
||||
<EventPresentationProvider value={{ layout: "group", density: "compact" }}>
|
||||
<Story />
|
||||
</EventPresentationProvider>
|
||||
),
|
||||
];
|
||||
WithCompactView.globals = {
|
||||
eventDensity: "compact",
|
||||
};
|
||||
|
||||
@@ -12,10 +12,10 @@ import classNames from "classnames";
|
||||
|
||||
import { useViewModel, type ViewModel } from "../../../../core/viewmodel";
|
||||
import { useI18n } from "../../../../core/i18n/i18nContext";
|
||||
import { useEventPresentation } from "../../EventPresentation";
|
||||
import type { UrlPreview } from "./types";
|
||||
import { LinkPreview } from "./LinkPreview";
|
||||
import styles from "./UrlPreviewGroupView.module.css";
|
||||
import { useEventPresentationAttributes } from "../../EventPresentation/EventPresentationContext";
|
||||
|
||||
/** Snapshot data for rendering URL previews attached to an event. */
|
||||
export interface UrlPreviewGroupViewSnapshot {
|
||||
@@ -62,7 +62,7 @@ export type UrlPreviewGroupViewModel = ViewModel<UrlPreviewGroupViewSnapshot, Ur
|
||||
*/
|
||||
export function UrlPreviewGroupView({ vm, className }: UrlPreviewGroupViewProps): JSX.Element | null {
|
||||
const { translate: _t } = useI18n();
|
||||
const { density } = useEventPresentation();
|
||||
const eventPresentationAttributes = useEventPresentationAttributes();
|
||||
const { previews, totalPreviewCount, previewsLimited, overPreviewLimit } = useViewModel(vm);
|
||||
if (previews.length === 0) {
|
||||
return null;
|
||||
@@ -80,8 +80,8 @@ export function UrlPreviewGroupView({ vm, className }: UrlPreviewGroupViewProps)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames(className, styles.wrapper)}>
|
||||
<div className={classNames(styles.previewGroup, density === "compact" && styles.compactLayout)}>
|
||||
<div className={classNames(className, styles.wrapper)} {...eventPresentationAttributes}>
|
||||
<div className={styles.previewGroup}>
|
||||
{previews.map((preview) => (
|
||||
<LinkPreview key={preview.link} onImageClick={() => vm.onImageClick(preview)} {...preview} />
|
||||
))}
|
||||
|
||||
@@ -4,6 +4,8 @@ exports[`UrlPreviewGroupView > renders a single preview 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_wrapper"
|
||||
data-event-density="default"
|
||||
data-event-layout="group"
|
||||
>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_previewGroup"
|
||||
@@ -82,6 +84,8 @@ exports[`UrlPreviewGroupView > renders multiple previews 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_wrapper"
|
||||
data-event-density="default"
|
||||
data-event-layout="group"
|
||||
>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_previewGroup"
|
||||
@@ -249,6 +253,8 @@ exports[`UrlPreviewGroupView > renders multiple previews which are hidden 1`] =
|
||||
<div>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_wrapper"
|
||||
data-event-density="default"
|
||||
data-event-layout="group"
|
||||
>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_previewGroup"
|
||||
@@ -336,9 +342,11 @@ exports[`UrlPreviewGroupView > renders with compact density 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_wrapper"
|
||||
data-event-density="default"
|
||||
data-event-layout="group"
|
||||
>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_previewGroup UrlPreviewGroupView-module_compactLayout"
|
||||
class="UrlPreviewGroupView-module_previewGroup"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
|
||||