Refactor Timeline Seperator (#31937)

* Refactor TimelineSeparator to shared-components package

  • New TimelineSeparator component in packages/shared-components/
  • Updated MessagePanel.tsx to import from shared-components

* Fix copyright text

* Timeline Unit Tests + Timeline Snapshot Tests

* Imported correct timeline seperator

* Update snapshots because of css update

* Apply suggestion from @florianduros

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>

* Created className prop

* Removal of element x unused css

* Update snapshot because of Flex

* Update snapshots because of Flex

* Update css to correct values and compund name

* Added letter spacing to timelineseperator

* rremoval of letter spacing

* added align center to flex to apply correct css changes

* Update snapshots to reflect new css changes

* Update snapshots to reflect css changes

* Added letter-spacing to timeline seperator

* Update snapshots after css update

* update snapshots

---------

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
This commit is contained in:
Zack
2026-02-04 14:25:36 +01:00
committed by GitHub
parent ea302162ee
commit c647c8ee3d
23 changed files with 321 additions and 78 deletions

View File

@@ -18,6 +18,7 @@ export * from "./event-tiles/TextualEventView";
export * from "./message-body/MediaBody";
export * from "./message-body/DecryptionFailureBodyView";
export * from "./message-body/ReactionsRowButtonTooltip";
export * from "./message-body/TimelineSeparator/";
export * from "./pill-input/Pill";
export * from "./pill-input/PillInput";
export * from "./room/RoomStatusBar";

View File

@@ -0,0 +1,21 @@
/*
* 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.
*/
.timelineSeparator {
clear: both;
margin: var(--cpd-space-1x) 0;
font: var(--cpd-font-body-md-regular);
letter-spacing: var(--cpd-font-letter-spacing-body-md);
color: var(--cpd-color-text-primary);
}
.timelineSeparator > hr {
flex: 1 1 0;
height: 0;
border: none;
border-bottom: 1px solid var(--cpd-color-gray-400);
}

View File

@@ -0,0 +1,54 @@
/*
* 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.
*/
import React from "react";
import type { Meta, StoryFn } from "@storybook/react-vite";
import TimelineSeparator from "./TimelineSeparator";
import styles from "./TimelineSeparator.module.css";
export default {
title: "MessageBody/TimelineSeparator",
component: TimelineSeparator,
tags: ["autodocs"],
args: {
label: "Label Separator",
children: "Timeline Separator",
},
} as Meta<typeof TimelineSeparator>;
const Template: StoryFn<typeof TimelineSeparator> = (args) => <TimelineSeparator {...args} />;
export const Default = Template.bind({});
export const WithHtmlChild = Template.bind({});
WithHtmlChild.args = {
label: "Custom Label",
children: (
<h2 className={styles.timelineSeparator} aria-hidden="true">
Thursday
</h2>
),
};
export const WithDateEvent = Template.bind({});
WithDateEvent.args = {
label: "Date Event Separator",
children: "Wednesday",
};
export const WithLateEvent = Template.bind({});
WithLateEvent.args = {
label: "Late Event Separator",
children: "Fri, Jan 9, 2026",
};
export const WithoutChildren = Template.bind({});
WithoutChildren.args = {
children: undefined,
label: "Separator without children",
};

View File

@@ -0,0 +1,48 @@
/*
* 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.
*/
import { render } from "@test-utils";
import { composeStories } from "@storybook/react-vite";
import React from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import * as stories from "./TimelineSeparator.stories.tsx";
const { Default, WithHtmlChild, WithoutChildren, WithDateEvent, WithLateEvent } = composeStories(stories);
describe("TimelineSeparator", () => {
afterEach(() => {
vi.clearAllMocks();
});
describe("Snapshot tests", () => {
it("renders the timeline separator in default state", () => {
const { container } = render(<Default />);
expect(container).toMatchSnapshot();
});
it("renders the timeline separator with HTML child", () => {
const { container } = render(<WithHtmlChild />);
expect(container).toMatchSnapshot();
});
it("renders the timeline separator with date event", () => {
const { container } = render(<WithDateEvent />);
expect(container).toMatchSnapshot();
});
it("renders the timeline separator with late event", () => {
const { container } = render(<WithLateEvent />);
expect(container).toMatchSnapshot();
});
it("renders the timeline separator without children", () => {
const { container } = render(<WithoutChildren />);
expect(container).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,54 @@
/*
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.
*/
import React, { type PropsWithChildren } from "react";
import classNames from "classnames";
import styles from "./TimelineSeparator.module.css";
import { Flex } from "../..";
/**
* Timeline separator props
*/
export interface TimelineSeparatorProps {
/**
* Accessible label for the separator (for example: "Today", "Yesterday", or a date).
*/
label: string;
/**
* The CSS class name.
*/
className?: string;
/**
* Optional children to render inside the timeline separator
*/
children?: PropsWithChildren["children"];
}
/**
* Generic timeline separator component to render within a MessagePanel
*
* @param label the accessible label string describing the separator
* @param children the children to draw within the timeline separator
*/
const TimelineSeparator: React.FC<TimelineSeparatorProps> = ({ label, className, children }) => {
// ARIA treats <hr/>s as separators, here we abuse them slightly so manually treat this entire thing as one
return (
<Flex
className={classNames(className, styles.timelineSeparator)}
role="separator"
aria-label={label}
align="center"
>
<hr role="none" />
{children}
<hr role="none" />
</Flex>
);
};
export default TimelineSeparator;

View File

@@ -0,0 +1,100 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`TimelineSeparator > Snapshot tests > renders the timeline separator in default state 1`] = `
<div>
<div
aria-label="Label Separator"
class="flex timelineSeparator"
role="separator"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
>
<hr
role="none"
/>
Timeline Separator
<hr
role="none"
/>
</div>
</div>
`;
exports[`TimelineSeparator > Snapshot tests > renders the timeline separator with HTML child 1`] = `
<div>
<div
aria-label="Custom Label"
class="flex timelineSeparator"
role="separator"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
>
<hr
role="none"
/>
<h2
aria-hidden="true"
class="timelineSeparator"
>
Thursday
</h2>
<hr
role="none"
/>
</div>
</div>
`;
exports[`TimelineSeparator > Snapshot tests > renders the timeline separator with date event 1`] = `
<div>
<div
aria-label="Date Event Separator"
class="flex timelineSeparator"
role="separator"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
>
<hr
role="none"
/>
Wednesday
<hr
role="none"
/>
</div>
</div>
`;
exports[`TimelineSeparator > Snapshot tests > renders the timeline separator with late event 1`] = `
<div>
<div
aria-label="Late Event Separator"
class="flex timelineSeparator"
role="separator"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
>
<hr
role="none"
/>
Fri, Jan 9, 2026
<hr
role="none"
/>
</div>
</div>
`;
exports[`TimelineSeparator > Snapshot tests > renders the timeline separator without children 1`] = `
<div>
<div
aria-label="Separator without children"
class="flex timelineSeparator"
role="separator"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
>
<hr
role="none"
/>
<hr
role="none"
/>
</div>
</div>
`;

View File

@@ -0,0 +1,8 @@
/*
* 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.
*/
export { default as TimelineSeparator, type TimelineSeparatorProps } from "./TimelineSeparator";